Feel free to share demo-related thoughts here.
If you have technical questions, please create a support ticket in the DevExpress Support Center.
Thank you for the feedback!
If you have technical questions, please create a support ticket in the DevExpress Support Center.
Backend API
@using DevExtreme.MVC.Demos.ViewModels
@using(Html.BeginForm()) {
using(Html.DevExtreme().ValidationGroup()) {
@Html.AntiForgeryToken()
@(Html.DevExtreme().Form<EditorsViewModel>()
.OnInitialized("onInitialized")
.OnOptionChanged("onOptionChanged")
.ShowValidationSummary(true)
.Items(items => {
items.AddGroup()
.Caption("Credentials")
.Items(groupItems => {
groupItems.AddSimpleFor(m => m.Email)
.Editor(e => e.TextBox()
.ValueChangeEvent("keyup")
);
groupItems.AddSimpleFor(m => m.Password)
.Editor(e => e.TextBox()
.Mode(TextBoxMode.Password)
.InputAttr("aria-label", "Password")
.OnValueChanged("passwordChanged")
.ValueChangeEvent("keyup")
.Buttons(buttons => {
buttons.Add()
.Name("password")
.Location(TextEditorButtonLocation.After)
.Widget(w => w.Button()
.Type(ButtonType.Default)
.Icon("eyeopen")
.StylingMode(ButtonStylingMode.Text)
.OnClick("() => changePasswordMode('Password')")
);
})
);
groupItems.AddSimpleFor(m => m.ConfirmPassword)
.Editor(e => e.TextBox()
.Mode(TextBoxMode.Password)
.InputAttr("aria-label", "Password")
.ValueChangeEvent("keyup")
.Buttons(buttons => {
buttons.Add()
.Name("password")
.Location(TextEditorButtonLocation.After)
.Widget(w => w.Button()
.Type(ButtonType.Default)
.Icon("eyeopen")
.StylingMode(ButtonStylingMode.Text)
.OnClick("() => changePasswordMode('ConfirmPassword')"));
})
);
});
items.AddGroup()
.Caption("Personal Data")
.Items(groupItems => {
groupItems.AddSimpleFor(m => m.Name)
.Editor(e => e.TextBox()
.ValueChangeEvent("keyup")
);
groupItems.AddSimpleFor(m => m.Date)
.Editor(e => e
.DateBox()
.OpenOnFieldClick(true)
.Placeholder("Birth Date")
.AcceptCustomValue(false)
);
groupItems.AddSimpleFor(m => m.VacationDates)
.Editor(e => e
.DateRangeBox()
.AcceptCustomValue(false)
.StartDatePlaceholder("Start Date")
.EndDatePlaceholder("End Date")
)
.ValidationRules(validationRule => {
validationRule.AddCustom()
.Message("Both start and end dates must be selected")
.ValidationCallback("validateVacationDatesPresence");
});
});
items.AddGroup()
.Caption("Billing address")
.Items(groupItems => {
groupItems.AddSimpleFor(m => m.Country)
.Editor(e => e
.SelectBox()
.InputAttr("aria-label", "Country")
.DataSource(d => d.WebApi().RouteName("GeoNames").LoadAction("Countries"))
);
groupItems.AddSimpleFor(m => m.City)
.Editor(e => e
.Autocomplete()
.ValueChangeEvent("keyup")
.MinSearchLength(2)
.DataSource(d => d.WebApi().RouteName("GeoNames").LoadAction("Cities"))
);
groupItems.AddSimpleFor(m => m.Address)
.Editor(e => e.TextBox()
.ValueChangeEvent("keyup")
);
groupItems.AddSimpleFor(m => m.Phone)
.HelpText("Enter the phone number in USA phone format")
.Editor(e => e.TextBox()
.Mask("+1 (X00) 000-0000")
.InputAttr("aria-label", "Phone")
.ValueChangeEvent("keyup")
.MaskRules(new { X = new JS("/[02-9]/") })
.MaskInvalidMessage("The phone must have a correct USA phone format")
);
});
items.AddGroup()
.CssClass("last-group")
.ColCountByScreen(c => c.Md(2).Sm(2).Lg(2))
.Items(groupItems => {
groupItems.AddSimpleFor(m => m.Accepted)
.Label(l => l.Visible(false))
.Editor(editor => editor.CheckBox()
.Width(270)
.Text("I agree to the Terms and Conditions")
);
groupItems.AddGroup()
.ColCountByScreen(c => c.Md(2).Sm(2).Lg(2))
.CssClass("buttons-group")
.Items(secondGroupItems => {
secondGroupItems.AddButton()
.Name("Reset")
.ButtonOptions(b => b.Text("Reset")
.Icon(Url.Content("refresh"))
.Width(120)
.Disabled(true)
.OnClick("onResetButtonClick")
);
secondGroupItems.AddButton()
.ButtonOptions(b => b.Text("Register")
.Type(ButtonType.Default)
.Width(120)
.UseSubmitBehavior(true)
);
});
});
})
.FormData(Model)
)
}
}
<script>
let formInstance;
function onInitialized(e) {
formInstance = e.component;
}
function onOptionChanged(e) {
if(e.name === 'isDirty') {
const resetButton = formInstance.getButton('Reset');
resetButton.option('disabled', !e.value);
}
}
function onResetButtonClick(e) {
formInstance.reset();
}
function passwordChanged(e) {
const editor = formInstance.getEditor('ConfirmPassword');
if (editor.option('value')) {
editor.element().dxValidator('validate');
}
}
function changePasswordMode(name) {
let editor = formInstance.getEditor(name);
editor.option('mode', editor.option('mode') === 'text' ? 'password' : 'text');
}
function validateVacationDatesPresence({ value }) {
const [startDate, endDate] = value;
if (startDate === null && endDate === null) {
return true;
}
return startDate !== null && endDate !== null;
}
</script>
<p>The submitted data has been successfully accepted.</p>
<br />
@(Html.DevExtreme().Button()
.Text("Reload demo")
.Type(ButtonType.Default)
.Icon("refresh")
.OnClick(@<text>
function() {
window.location = window.location;
}
</text>)
)
using DevExtreme.AspNet.Data;
using DevExtreme.AspNet.Mvc;
using DevExtreme.MVC.Demos.Models.SampleData;
using DevExtreme.MVC.Demos.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Web.Mvc;
namespace DevExtreme.MVC.Demos.Controllers {
public class FormController : Controller {
[HttpGet]
public ActionResult Validation() {
return View(new EditorsViewModel() {
Name = "Peter"
});
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Validation(EditorsViewModel userInfo) {
if(ModelState.IsValid) {
return View("SuccessValidation");
}
return View(userInfo);
}
}
}
using DevExtreme.AspNet.Data;
using DevExtreme.AspNet.Mvc;
using DevExtreme.MVC.Demos.Models.SampleData;
using System;
using System.Linq;
using System.Net.Http;
using System.Web.Http;
namespace DevExtreme.MVC.Demos.Controllers.ApiControllers {
[Route("api/GeoNames/{action}", Name = "GeoNames")]
public class GeoNamesController : ApiController {
[HttpGet]
public HttpResponseMessage Countries(DataSourceLoadOptions loadOptions) {
return Request.CreateResponse(DataSourceLoader.Load(SampleData.Countries, loadOptions));
}
[HttpGet]
public HttpResponseMessage Cities(DataSourceLoadOptions loadOptions) {
return Request.CreateResponse(DataSourceLoader.Load(SampleData.Cities, loadOptions));
}
}
}
using System;
using System.Linq;
using System.Web.Mvc;
using DevExtreme.MVC.Demos.Models.DataGrid;
namespace DevExtreme.MVC.Demos.Controllers {
public class RemoteValidationController : Controller {
InMemoryEmployeesValidationDataContext db = new InMemoryEmployeesValidationDataContext();
[HttpPost]
public JsonResult CheckUniqueEmailAddress(EmployeeValidation model) {
var isValid = !db.Employees.Any(emp => {
var equals = string.Equals(emp.Email, model.Email, StringComparison.OrdinalIgnoreCase);
return model.ID != emp.ID && equals;
});
return Json(isValid);
}
[HttpPost]
public JsonResult CheckEmailAddress(string email) {
var isValid = !string.Equals(email, "test@dx-email.com", StringComparison.OrdinalIgnoreCase);
return Json(isValid);
}
}
}
using DevExtreme.AspNet.Mvc;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace DevExtreme.MVC.Demos.ViewModels {
public class EditorsViewModel {
[Required(ErrorMessage = "Email is required")]
[RegularExpression(@"^[\d\w._-]+@[\d\w._-]+\.[\w]+$", ErrorMessage = "Email is invalid")]
[Remote("CheckEmailAddress", "RemoteValidation", ErrorMessage = "Email is already registered", HttpMethod = "POST")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Name is required")]
[RegularExpression(@"^[^0-9]+$", ErrorMessage = "Do not use digits in the Name.")]
[StringLength(int.MaxValue, MinimumLength = 2, ErrorMessage = "Name must have at least 2 symbols")]
public string Name { get; set; } = string.Empty;
[Required(ErrorMessage = "Password is required")]
public string Password { get; set; } = string.Empty;
[Required(ErrorMessage = "Confirm Password is required")]
[System.ComponentModel.DataAnnotations.Compare("Password", ErrorMessage = "'Password' and 'Confirm Password' do not match.")]
public string ConfirmPassword { get; set; } = string.Empty;
[RegularExpression(@"^[02-9]\d{9}$", ErrorMessage = "The phone must have a correct USA phone format")]
public string Phone { get; set; } = string.Empty;
public string Extension { get; set; }
[Required(ErrorMessage = "Country is required")]
public string Country { get; set; }
[Required(ErrorMessage = "Address is required")]
public string Address { get; set; } = string.Empty;
public string Description { get; set; }
public int Age { get; set; }
public string Drink { get; set; }
[Required(ErrorMessage = "City is required")]
[RegularExpression("^[^0-9]+$", ErrorMessage = "Do not use digits in the City name.")]
[StringLength(int.MaxValue, MinimumLength = 2, ErrorMessage = "City must have at least 2 symbols")]
public string City { get; set; }
public IEnumerable<string> Colors { get; set; }
public IEnumerable<string> SelectedColors { get; set; }
public string Color { get; set; }
[Display(Name = "Date of birth")]
[Required(ErrorMessage = "Date of birth is required")]
[VerifyAge(21, ErrorMessage = "You must be at least {1} years old")]
public DateTime? Date { get; set; }
[VerifyDateRange(25, ErrorMessage = "The vacation period must not exceed {1} days")]
public DateTime?[] VacationDates { get; set; }
[DevExtremeRequired(ErrorMessage = "You must agree to the Terms and Conditions")]
public bool Accepted { get; set; }
}
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
namespace DevExtreme.MVC.Demos.ViewModels {
public class VerifyAgeAttribute : ValidationAttribute, IClientValidatable {
public VerifyAgeAttribute(int age) {
Age = age;
}
public int Age { get; private set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
if((DateTime?)value <= DateTime.Now.AddYears(-Age)) {
return ValidationResult.Success;
}
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
IEnumerable<ModelClientValidationRule> IClientValidatable.GetClientValidationRules(ModelMetadata metadata, ControllerContext context) {
var rule = new ModelClientValidationRule();
rule.ErrorMessage = FormatErrorMessage(metadata.GetDisplayName());
rule.ValidationParameters.Add(
"validationcallback",
$@"function(options) {{
var now = new Date();
return options.value && options.value <= now.setFullYear(now.getFullYear() - {Age});
}}");
rule.ValidationType = "custom";
yield return rule;
}
public override string FormatErrorMessage(string name) {
return string.Format(ErrorMessageString, name, Age);
}
}
}
form {
margin: 10px 10px 15px;
}
.last-group {
margin-top: 30px;
margin-bottom: 10px;
}
.last-group .dx-item-content {
align-items: start;
justify-content: center;
}
.last-group .dx-field-item {
padding: 0 !important;
}
.buttons-group {
display: flex;
width: 100%;
justify-content: end;
}
.buttons-group .dx-item-content {
gap: 10px;
}
Note that the "Register" button here does not implement the usual onClick event handler. Instead, it has the useSubmitBehavior property set to true. This setting tells the button to validate and submit the HTML form in which it is nested, with no further configuration required.