I'm trying to work out how to validate a deeply nested form structure (sections/panels/fields) where the fields themselves are stored as a dictionary. I have managed to fix up the paths and such so that the standard ValidationMessage componet displays a reasonable human readable error, and all of my validations do appear to function, unfortunately the Css Class setting has ceased to function.
When using a simple form layer (unnested Poco) the classes are applied automatically - my SyncFusion controls receive their 'e-error' or 'e-success' classes and change colour accordingly - when using a home made Validator, the colours don't function any more. I also tried setting my own CssClassProvider with EditContext.SetFieldCssClassProvider, and a breakpoint on the GetFieldCssClass function is never hit.
Effectively whilst my calculated FieldIdentifiers work correctly with ValidationMessage, that doesn't lead to any kind of Css Update.
Is there some kind of trigger that needs to be called from my FluentValidation Validator to kick off the CssClass mechanism?
Here's some code - note that the EditForm was in the parent page of PanelLayout and it's Model is of type PanelLayoutData - I didn't want to have to paste all of that.
PanelLayout.razor
<article class="st-vscroll bg-body pb-5 st-theme-#Theme">
<PanelLayoutValidator />
<ValidationSummary />
#{
int sectIndex = 0;
foreach(var panel in Data.panelData) {
int panIndex = 0;
while (panIndex < panel.Panels.Count)
{
var pan1 = panel.Panels[panIndex++];
<FieldListPanel Title=#pan1.Title DataDictionary=#(pan1.DisplayDictionary) LabelAbove=#true />
}
}
}
</article>
#code {
[CascadingParameter(Name = "Theme")] public string Theme { get; set; }
[CascadingParameter(Name = "EditMode")] public bool EditMode { get; set; }
[CascadingParameter] public EditContext EditContext { get; set; }
[Parameter] public clientmodels.PanelLayoutData Data { get; set; } = null;
protected override void OnParametersSet()
{
sectionRefs = new ElementReference[Data.panelData.Count];
if (EditContext != null) {
EditContext.SetFieldCssClassProvider(new _PanelLayoutFieldCssClassProvider());
}
}
}
FieldListPanel.razor
<div class="card rounded-0 border-0">
#if (!string.IsNullOrEmpty(Title))
{
<div class="card-header border-0 mt-3">
<h3 class="display-6">#Title</h3>
</div>
}
<div class="card-body">
#if (DataDictionary?.Any() ?? false)
{
#foreach (var kv in DataDictionary) {
<div class="row mb-3">
#if (!(LabelAbove && kv.Value?.DisplayName == ""))
{
<div class=#((LabelAbove ? "col-12" : "col-4"))>
#(kv.Value?.DisplayName ?? kv.Key)#if (kv.Value.IsRequired) { <span style="required-value">*</span> }
</div>
}
<div class=#((LabelAbove ? "col-12" : "col-8"))>
#if (kv.Value?.Template != null)
{
#kv.Value?.Template
} else
{
//When there is no template then it's just text - rendering in edit mode will require a text box
if (Editable && EditMode && kv.Value.IsEditable)
{
<SfTextBox Value=#kv.Value.Value ValueChange="#((__v) => updateDictValue(__v, kv.Key))" /><br />
<ValidationMessage For="() => kv.Value.Value" />
} else
{
#kv.Value?.Value
}
}
</div>
</div>
}
}
</div>
</div>
#code {
[Parameter]
public string Title { get; set; } = "";
[Parameter]
public IDictionary<string, clientmodels.FieldDisplayData> DataDictionary { get; set; } = null;
[Parameter]
public bool LabelAbove { get; set; } = false;
[Parameter] public bool Editable { get; set; } = true;
[CascadingParameter(Name = "EditMode")] public bool EditMode { get; set; }
[Parameter]
public EventCallback OnChanged { get; set; }
private async Task updateDictValue(ChangedEventArgs e, string key)
{
if (DataDictionary.ContainsKey(key))
{
DataDictionary[key].Value = e.Value;
await OnChanged.InvokeAsync();
}
}
}
PanelLayoutValidator.cs
namespace CustomStyle.Client.Code
{
public class _PanelLayoutValidationState
{
public string FullPath { get; set; }
}
public class _PanelLayoutFieldCssClassProvider : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
{
var isValid = !editContext.GetValidationMessages(fieldIdentifier).Any();
return isValid ? "e-success" : "e-error";
}
}
public class _PanelLayoutValidator : AbstractValidator<PanelLayoutData>
{
public _PanelLayoutValidator()
{
RuleForEach(l => l.panelData)
.ChildRules(l => {
l.RuleForEach(s => s.Panels)
.ChildRules(s => {
//We apply rules for [Required] [MinLength] [MaxLength] [CreditCard] [EmailAddress] [Range] [RegularExpression]
//Configure property names
var ddeach = s.RuleForEach(p => p.DisplayDictionary);
ddeach.ChildRules(kvconfig =>
{
kvconfig.RuleFor(kv => kv.Value.Value).Configure(cfg =>
{
cfg.MessageBuilder = context =>
{
context.MessageFormatter.AppendPropertyName(context.PropertyName);
return context.GetDefaultMessage();
};
});
});
//Non parametric validations
ddeach.ChildRules(kvconfig =>
{
//IsRequired
kvconfig.RuleFor(kv => kv.Value.Value)
.NotEmpty().When(x => x.Value != null && x.Value.IsRequired).WithMessage("{ParsedPropertyName} cannot be empty");
//CreditCard
kvconfig.RuleFor(kv => kv.Value.Value)
.CreditCard().When(x => x.Value != null && x.Value.IsCreditCard).WithMessage("{ParsedPropertyName} should be a Credit or Debit card number");
//EmailAddress
kvconfig.RuleFor(kv => kv.Value.Value)
.EmailAddress().When(x => x.Value != null && x.Value.IsCreditCard).WithMessage("{ParsedPropertyName} should be an Email Address");
});
//Parametric validations
ddeach.ChildRules(kvconfig =>
{
//MinLength
kvconfig.RuleFor(kv => new { Value = kv.Value.Value, Config = kv.Value })
.Must(vl => vl.Value.Length >= vl.Config.MinLength)
.When(x => x.Value != null && x.Value.MinLength != null)
.WithMessage(x => $"{{ParsedPropertyNameVV}} must have at least {x.Value.MinLength} characters");
//MaxLength
kvconfig.RuleFor(kv => new { Value = kv.Value.Value, Config = kv.Value })
.Must(vl => vl.Value.Length <= vl.Config.MaxLength)
.When(x => x.Value != null && x.Value.MaxLength != null)
.WithMessage(x => $"{{ParsedPropertyNameVV}} must have at most {x.Value.MaxLength} characters");
//Range
kvconfig.RuleFor(kv => new { Value = int.Parse(kv.Value.Value), Config = kv.Value })
.Must(vl => vl.Value >= vl.Config.Range[0] && vl.Value <= vl.Config.Range[1])
.When(x => x.Value != null && x.Value.Range != null)
.WithMessage(x => $"{{ParsedPropertyNameVV}} must be between {x.Value.Range[0]} and {x.Value.Range[1]}");
//Regex
kvconfig.RuleFor(kv => new { Value = kv.Value.Value, Config = kv.Value })
.Must(vl => System.Text.RegularExpressions.Regex.IsMatch(vl.Value, vl.Config.RegularExpression))
.When(x => x.Value != null && x.Value.RegularExpression != null)
.WithMessage(x => $"{{ParsedPropertyNameVV}} does not match the expected pattern");
});
//ToDo: Add rules for RefPoco routes based on object data annotation attributes
});
});
}
}
public class PanelLayoutValidator : ComponentBase
{
private readonly static char[] separators = new[] { '.', '[' };
private _PanelLayoutValidator validator;
[CascadingParameter] private EditContext EditContext { get; set; }
protected override void OnInitialized()
{
validator = new _PanelLayoutValidator();
var messages = new ValidationMessageStore(EditContext);
// Revalidate when any field changes, or if the entire form requests validation
// (e.g., on submit)
EditContext.OnFieldChanged += (sender, eventArgs)
=> ValidateModel((EditContext)sender, messages);
EditContext.OnValidationRequested += (sender, eventArgs)
=> ValidateModel((EditContext)sender, messages);
}
private string GetParsedPropertyName(EditContext context, FieldIdentifier id, string PropertyName)
{
//process the property path to calculate the property description
//If we're using the expected format for a dictionary field, we can read the display name and the key
var model = context.Model as PanelLayoutData;
var match = System.Text.RegularExpressions.Regex.Match(PropertyName, #"^panelData\[(?<section_index>[^\]]+)\].Panels\[(?<panel_index>[^\]]+)\].DisplayDictionary\[(?<field_key>[^\]]+)\].(?<target>Value|RefPoco)");
if (match.Success)
{
var section_index = int.Parse(match.Groups["section_index"].Value);
var section_name = model.panelData[section_index].Title;
var panel_index = int.Parse(match.Groups["panel_index"].Value);
var panel_name = model.panelData[section_index].Panels[panel_index].Title;
var property_name = "";
if (match.Groups["target"].Value == "Value")
{
var field_key_index = int.Parse(match.Groups["field_key"].Value);
var dict = model.panelData[section_index].Panels[panel_index].DisplayDictionary;
var field_key = dict.Keys.Skip(field_key_index).First();
property_name = dict[field_key].DisplayName ?? field_key;
} else
{
//TODO: Expand this to grab the property and look for DisplayName attributes
property_name = id.FieldName;
}
if (!string.IsNullOrEmpty(section_name)) {
section_name += "/";
}
if (!string.IsNullOrEmpty(panel_name))
{
panel_name += "/";
}
return $"{section_name}{panel_name}{property_name}";
} else {
//we have no section info so just pick up the property name
//TODO: Expand this to grab the property and look for DisplayName attributes
return id.FieldName;
}
}
private void ValidateModel(EditContext editContext, ValidationMessageStore messages)
{
var validationResult = validator.Validate((PanelLayoutData)editContext.Model);
messages.Clear();
foreach (var error in validationResult.Errors)
{
FieldIdentifier fieldIdentifier = default(FieldIdentifier);
var msg = error.ErrorMessage;
if (msg.Contains("{ParsedPropertyName}")) {
fieldIdentifier = ToFieldIdentifier(editContext, error.PropertyName);
msg = msg.Replace("{ParsedPropertyName}", GetParsedPropertyName(editContext, fieldIdentifier, error.PropertyName));
}
if (msg.Contains("{ParsedPropertyNameVV}"))
{
fieldIdentifier = ToFieldIdentifier(editContext, $"{error.PropertyName}.Value");
msg = msg.Replace("{ParsedPropertyNameVV}", GetParsedPropertyName(editContext, fieldIdentifier, $"{error.PropertyName}.Value"));
}
messages.Add(fieldIdentifier, msg);
}
editContext.NotifyValidationStateChanged();
}
private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath)
{
// This method parses property paths like 'SomeProp.MyCollection[123].ChildProp'
// and returns a FieldIdentifier which is an (instance, propName) pair. For example,
// it would return the pair (SomeProp.MyCollection[123], "ChildProp"). It traverses
// as far into the propertyPath as it can go until it finds any null instance.
var obj = editContext.Model;
while (true)
{
var nextTokenEnd = propertyPath.IndexOfAny(separators);
if (nextTokenEnd < 0)
{
return new FieldIdentifier(obj, propertyPath);
}
var nextToken = propertyPath.Substring(0, nextTokenEnd);
propertyPath = propertyPath.Substring(nextTokenEnd + 1);
object newObj;
if (nextToken.EndsWith("]"))
{
nextToken = nextToken.Substring(0, nextToken.Length - 1);
var tobj = obj.GetType();
if (obj is IDictionary)
{
//fluent validation indicates index in dictionary as an integer - dictionaries don't index like that
//grab the key at the given index
var kprop = tobj.GetProperty("Keys");
var keys = (ICollection)kprop.GetValue(obj);
object key = keys.Cast<object>().Skip(int.Parse(nextToken)).First();
var prop = tobj.GetProperty("Item");
newObj = prop.GetValue(obj, new object[] { key });
}
else
{
// It's an indexer
// This code assumes C# conventions (one indexer named Item with one param)
var prop = tobj.GetProperty("Item");
var indexerType = prop.GetIndexParameters()[0].ParameterType;
var indexerValue = Convert.ChangeType(nextToken, indexerType);
newObj = prop.GetValue(obj, new object[] { indexerValue });
}
}
else
{
// It's a regular property
var prop = obj.GetType().GetProperty(nextToken);
if (prop == null)
{
throw new InvalidOperationException($"Could not find property named {nextToken} on object of type {obj.GetType().FullName}.");
}
newObj = prop.GetValue(obj);
}
if (newObj == null)
{
// This is as far as we can go
return new FieldIdentifier(obj, nextToken);
}
obj = newObj;
}
}
}
}
PanelModels.cs
namespace CustomStyle.Client.Models
{
public class PanelData
{
public string Title { get; set; }
public Dictionary<string, FieldDisplayData> DisplayDictionary { get; set; } = new();
public bool IsFullWidth { get; set; } = false;
public bool IsSpacer { get; set; } = false;
public bool LabelAbove { get; set; } = false;
}
public class PanelSectionData
{
[Required]
public string Title { get; set; }
public List<PanelData> Panels { get; set; } = new();
}
public class PanelLayoutData : PageLayoutData
{
public string idPrefix { get; set; }
public List<PanelSectionData> panelData { get; set; } = new();
}
public class FieldDisplayData
{
public FieldDisplayData()
{
}
public FieldDisplayData(string value, bool isRequired = false)
{
DisplayName = null;
Value = value;
Template = null;
IsRequired = isRequired;
}
public FieldDisplayData(RenderFragment template, bool isRequired = false)
{
DisplayName = null;
Value = null;
Template = template;
IsRequired = isRequired;
}
public FieldDisplayData(string displayName, string value, bool isRequired = false)
{
DisplayName = displayName;
Value = value;
Template = null;
IsRequired = isRequired;
}
public FieldDisplayData(string displayName, RenderFragment template, bool isRequired = false)
{
DisplayName = displayName;
Value = null;
Template = template;
IsRequired = isRequired;
}
public string DisplayName { get; set; } = null;
public string Value { get; set; } = null;
public RenderFragment Template { get; set; } = null;
//If the Template references a child of the overall panel data tree,
//add a reference here to allow the validator to see it
public object RefPoco { get; set; } = null;
public bool IsEditable { get; set; } = true;
public bool IsRequired { get; set; } = false;
public int? MinLength { get; set; } = null;
public int? MaxLength { get; set; } = null;
}
}
Hopefully somebody might just know what I messed up.
Thanks.
This is how things work in the standard Blazor Input Controls, which is probably similar to the SynFusion controls (but as they are proprietary the jury is out).
The input control builds a FieldIdentifier from the ValueExpression that either you provide manually or the Razor compiler builds for you from a #bind-Value definition. When a component renders, it uses this FieldIdentifier to check for validation messages in the Validation Store and then applies the neccessary Css settings through a FieldCssClassProvider.
So either:
Your input components aren't re-rendering when the validation state changes in the form, or
The FieldIdentifier constructed by the input control doesn't match the one used to identify the validation message in the Validation Store, or
The Syncfusion controls operate to a different set of rules.
As I don't use Syncfusion and you're question is a bit of a wall of code without context I can only offer pointers as to where the problem might be. Hopefully someone with Syncfusion knowledge will provide a more solid answer. Good luck.
Related
I'm tired to search on web a solution for this. Basicly i am using InputBase to extend the normal inputbox to a custom component. For single selection in its ok, but turns complicate when i have mutiple selection "select multiple="multiple""
So here is the code:
File: XDropDownMultiSelect.razor
#using System.Linq.Expressions
#typeparam T
#inherits InputBase<T>
#if (!string.IsNullOrEmpty(Label))
{
<label class="form-label">#Label</label>
}
<select #bind="CurrentValue" class="form-control select2 #CssClass" id="#Id" #attributes="AdditionalAttributes" multiple>
#if (DropdownValues != null)
{
foreach (var cursor in DropdownValues)
{
<option value="#cursor.Key">#cursor.Value</option>
}
}
</select>
#code {
[Inject] public IJSRuntime _js { get; set; }
[Parameter, EditorRequired] public string Id { get; set; }
[Parameter] public string Label { get; set; }
[Parameter] public Expression<Func<T>> ValidationFor { get; set; }
[Parameter] public bool ShowDefaultOption { get; set; } = true;
[Parameter] public Dictionary<string, string> DropdownValues { get; set; }
[Parameter] public string Selected { get; set; }
protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage)
{
if (typeof(T) == typeof(string))
{
result = (T)(object)value;
validationErrorMessage = null;
return true;
}
else if (typeof(T) == typeof(string[]))
{
result = (T)(object)(new string[] { value });
validationErrorMessage = null;
return true;
}
else if (typeof(T) == typeof(Guid))
{
Guid.TryParse(value, out var parsedValue);
result = (T)(object)parsedValue;
validationErrorMessage = null;
return true;
}
else if (typeof(T).IsEnum)
{
try
{
result = (T)Enum.Parse(typeof(T), value);
validationErrorMessage = null;
return true;
}
catch (ArgumentException)
{
result = default;
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
return false;
}
}
throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(T)}'.");
}
}
I use "CurrentValue" instead of "CurrentValueAsString" because is an array no string, and if i set CurrentValueAsString will have a render json problem...
Now i call by simple:
<XDropDownMultiSelect #bind-Value="usersSelected" Id="test" DropdownValues="usersAll" />
#code{
public string[] usersSelected { get; set; } = new [] { "user1" };
public string[] usersAll{ get; set; } = new [] { "user1", "user2", "user3" };
Its working, but dosenĀ“t bind the new selection values to my selectValues object.
i found how to use CurrentValueAsString
protected override string FormatValueAsString(T? value)
{
if( value != null)
{
return value.ToJson(); //this is extension my to convert any object to json format.
}
return base.FormatValueAsString(value);
}
But this not update the source model with new selections.
Context:
I have an angular App which send complex Data to my .net Core API. Data sent could be an Image (ImageDto) or a couple of Images (StackImageDto), and Title & Tags. We send Data using multi-part HTML form, It means the Data is not JSON formatted.
Bug:
There is currently a bug in .Net Core 2.2 with BindModelAsync:
https://github.com/aspnet/AspNetCore/issues/6616
Similar issues:
I try to follow what people suggested but I didnt succeed to make it work correctly:
https://github.com/aspnet/Mvc/issues/4703
http://qaru.site/questions/1630337/mvc-6-custom-model-binder-with-dependency-injection
Model binder for abstract class in asp.net core mvc 2
Code:
As I need to make the API works, I use a different way to set my abstract class DTO used in the controller.
Here is the DTO classes, MediaDto:
[ModelBinder(BinderType = typeof(AbstractModelBinder))]
public abstract class MediaDto
{
public Guid? Id { get; set; }
[Required]
public string Title { get; set; }
public string Note { get; set; }
public TagDto[] Tags { get; set; } = new TagDto[] { };
public ChallengeDetailsDto[] Challenges { get; set; }
[Required]
public string Discriminator { get; set; }
public DateTimeOffset? CreatedAt { get; set; }
public string CreatedBy { get; set; }
//public Guid CreatedByUserId { get; set; }
public DateTimeOffset? EditedAt { get; set; }
public string EditedBy { get; set; }
public DateTimeOffset? DeletedAt { get; set; }
public string DeletedBy { get; set; }
}
ImageDto:
[Serializable]
public class ImageDto : MediaDto
{
public IFormFile Image { get; set; }
public string Url { get; set; }
public ImageDto()
{
Discriminator = ImageDtoDiscriminator;
}
}
StackImageDto:
[Serializable]
public class StackImageDto : MediaDto
{
[Required]
[EnumDataType(typeof(StackImageType))]
public StackImageType Type { get; set; }
[Required]
public IEnumerable<StackFileDto> StackFiles { get; set; }
public StackImageDto()
{
Discriminator = StackImageDtoDiscriminator;
}
}
StackFileDto
[Serializable]
public class StackFileDto
{
public Guid? Id { get; set; }
public IFormFile Image { get; set; }
public string Url { get; set; }
public int Position { get; set; }
}
MediaLibraryConverter convert the Json to my instance of MediaDto:
public class MediaLibraryConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(MediaDto).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
MediaDto result = null;
var jsonChallange = JObject.Load(reader);
var discriminator = jsonChallange.Properties().First(p => p.Name == "discriminator").Value.ToString();
if (discriminator.Equals(nameof(ImageDto), StringComparison.InvariantCultureIgnoreCase))
{
result = (ImageDto)JsonConvert.DeserializeObject(jsonChallange.ToString(), typeof(ImageDto));
}
else if (discriminator.Equals(nameof(StackImageDto), StringComparison.InvariantCultureIgnoreCase))
{
result = (StackImageDto)JsonConvert.DeserializeObject(jsonChallange.ToString(), typeof(StackImageDto));
}
else
{
throw new ApiException($"{nameof(MediaDto)} discriminator is not set properly.", HttpStatusCode.BadRequest);
}
return result;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
AbstractModelBinder does the context binding, as I get the data using [FromForm] attribute in the action of the controller:
public class AbstractModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));
// Fetch the value of the argument by name and set it to the model state
string fieldName = bindingContext.FieldName;
try
{
var data = ExtractDataFromContext(bindingContext)[0];
var jsonData = JsonConvert.SerializeObject(data);
bindingContext.ModelState.SetModelValue(fieldName, new ValueProviderResult(jsonData));
var discriminator = JObject.Parse(jsonData)
.Properties()
.First(p => p.Name == "discriminator")
.Value
.ToString();
// Deserialize the provided value and set the binding result
if (discriminator.Equals(nameof(ImageDto), StringComparison.InvariantCultureIgnoreCase))
{
object o = JsonConvert.DeserializeObject<ImageDto>(jsonData, new MediaLibraryConverter());
o = BindFormFiles(o, bindingContext);
bindingContext.Result = ModelBindingResult.Success(o);
}
else if (discriminator.Equals(nameof(StackImageDto), StringComparison.InvariantCultureIgnoreCase))
{
object o = JsonConvert.DeserializeObject<StackImageDto>(jsonData, new MediaLibraryConverter());
o = BindFormFiles(o, bindingContext);
bindingContext.Result = ModelBindingResult.Success(o);
}
}
catch (JsonException e)
{
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
BindFormFiles is a recursive function helping to bind json and IFormFile getting from HttpContext to an Instance of MediaDto
private object BindFormFiles(object tmp, ModelBindingContext bindingContext, string fieldName = null)
{
if (tmp == null)
{
return tmp;
}
var type = tmp.GetType();
if (type.IsPrimitive || typeof(string).IsAssignableFrom(type))
{
return tmp;
}
if (typeof(IEnumerable).IsAssignableFrom(type) && !typeof(IEnumerable<IFormFile>).IsAssignableFrom(type))
{
var array = (IList)tmp;
for (int i = 0; i < array.Count; i++)
{
array[i] = BindFormFiles(array[i], bindingContext, $"{fieldName}[{i}]");
}
}
else
{
foreach (var property in tmp.GetType().GetProperties())
{
var name = property.Name;
if (typeof(IFormFile).IsAssignableFrom(property.PropertyType))
{
var file = bindingContext.HttpContext.Request.Form.Files
.FirstOrDefault(f => string.IsNullOrEmpty(fieldName)
? f.Name.Equals($"{property.Name}", StringComparison.OrdinalIgnoreCase)
: f.Name.Equals($"{fieldName}.{property.Name}", StringComparison.OrdinalIgnoreCase)
);
if (file != null)
{
property.SetValue(tmp, file);
}
}
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
{
if (property.PropertyType == typeof(IEnumerable<IFormFile>))
{
var files = bindingContext.HttpContext.Request.Form.Files
.Where(f => string.IsNullOrEmpty(fieldName)
? f.Name.Equals($"{property.Name}", StringComparison.OrdinalIgnoreCase)
: f.Name.Equals($"{fieldName}.{property.Name}", StringComparison.OrdinalIgnoreCase)
);
if (files?.Any() == true)
{
property.SetValue(tmp, files);
}
}
else
{
property.SetValue(
tmp,
BindFormFiles(
property.GetValue(tmp),
bindingContext,
string.IsNullOrEmpty(fieldName)
? property.Name
: $"{fieldName}.{property.Name}"
)
);
}
}
else if (property.PropertyType.IsClass && !typeof(string).IsAssignableFrom(property.PropertyType))
{
property.SetValue(
tmp,
BindFormFiles(
property.GetValue(tmp),
bindingContext,
property.Name
)
);
}
}
}
return tmp;
}
ExtractDataFromContext is a recursive function extracting Data from HttpContext
private List<Dictionary<string, object>> ExtractDataFromContext(ModelBindingContext bindingContext, string fieldName = null)
{
var i = 0;
var propertyName = string.IsNullOrEmpty(fieldName)
? string.Empty
: $"{fieldName}[{i}]";
var result = new List<Dictionary<string, object>>();
while (bindingContext.ActionContext.HttpContext.Request.Form
.Where(kv => string.IsNullOrEmpty(propertyName)
? true
: kv.Key.StartsWith(propertyName, StringComparison.OrdinalIgnoreCase)
)
.DistinctBy(kv => kv.Key.Substring(0, propertyName.Length))
.Any()
)
{
var values = bindingContext.ActionContext.HttpContext.Request.Form
.Where(kv => string.IsNullOrEmpty(propertyName)
? true
: kv.Key.StartsWith($"{fieldName}[{i}]", StringComparison.OrdinalIgnoreCase)
)
.ToDictionary(
kv => string.IsNullOrEmpty(propertyName)
? kv.Key
: kv.Key.Replace($"{propertyName}.", string.Empty, StringComparison.OrdinalIgnoreCase),
kv => (object)kv.Value[0]
);
var subValues = values
.Where(kv => kv.Key.Contains("]."))
.GroupBy(kv => kv.Key.Substring(0, kv.Key.IndexOf("[")))
.ToDictionary(
g => g.Key,
g => string.IsNullOrEmpty(propertyName)
? (object)ExtractDataFromContext(bindingContext, $"{g.Key}")
: (object)ExtractDataFromContext(bindingContext, $"{propertyName}.{g.Key}")
)
.Concat(values
.Where(kv => kv.Key.Contains("]") && !kv.Key.Contains("]."))
.GroupBy(kv => kv.Key.Substring(0, kv.Key.IndexOf("[")))
.ToDictionary(
g => g.Key,
g => (object)g.Select(kv => kv.Value)
)
);
result.Add(values
.Where(kv => !kv.Key.Contains("]"))
.Concat(subValues)
.ToList()
.ToDictionary(
kv => kv.Key,
kv => kv.Value
)
);
i++;
propertyName = $"{fieldName}[{i}]";
}
return result;
}
For people interested to know what the HttpContext.Request.Form contains:
Files:
Keys:
ValidateModelStateAttribute is an ActionFilterAttribute validating the current ModelState for all controller actions
[AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
public class ValidateModelStateAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
As you can see, the ModelState is not valid not because of errors but because the ValidationState is 'unvalidated'.
If I bypass the ActionFilter and go in the action of my controller, I can see the Dto correctly filled:
Question:
How can I say to context.ModelState that his ValidationState is valid?
In a project there is a reporting class as follows:
public class ReportRowDataContract
{
public ReportDataDataContract ReportData1 { get; set; }
public ReportDataDataContract ReportData2 { get; set; }
public ReportDataDataContract ReportData3 { get; set; }
public ReportDataDataContract ReportData4 { get; set; }
public ReportDataDataContract ReportData5 { get; set; }
public ReportDataDataContract ReportData6 { get; set; }
}
Then there is a method that works with objects from the above class. Here is the first part of this method:
public ReportGrid(List<ReportRowDataContract> items , List<ReportDataDataContract> summaryData)
: base(items)
{
passedInSummaryData = summaryData;
if (items[0].ReportData1 != null)
{
if (items[0].ReportData1.DecimalValue != null)
{
Columns.Add(m => m.ReportData1.DecimalValue).Titled(items[0].ReportData1.Name).Encoded(false).
Sanitized(false).RenderValueAs(
m => (string.IsNullOrEmpty(#m.ReportData1.DisplayFormat)) ? Convert.ToDecimal(#m.ReportData1.DecimalValue).ToString("N") : Convert.ToDecimal(#m.ReportData1.DecimalValue).ToString(#m.ReportData1.DisplayFormat));
if (items[0].ReportData1.SumValue || items[0].ReportData1.AvgValue)
{
displaySummary = true;
SummaryData.Add(
new ReportDataDataContract
{
Name = items[0].ReportData1.Name,
AvgValue = items[0].ReportData1.AvgValue,
DecimalValue = 0
});
}
}
else if (items[0].ReportData1.IntValue != null)
{
Columns.Add(m => m.ReportData1.IntValue).Titled(items[0].ReportData1.Name);
if (items[0].ReportData1.SumValue || items[0].ReportData1.AvgValue)
{
displaySummary = true;
SummaryData.Add(
new ReportDataDataContract
{
Name = items[0].ReportData1.Name,
AvgValue = items[0].ReportData1.AvgValue,
IntValue = 0
});
}
}
else
{
Columns.Add(m => m.ReportData1.StringValue).Titled(items[0].ReportData1.Name);
}
}
if (items[0].ReportData2 != null)
{
if (items[0].ReportData2.DecimalValue != null)
{
Columns.Add(m => m.ReportData2.DecimalValue).Titled(items[0].ReportData2.Name).Encoded(false).
Sanitized(false).RenderValueAs(
m => (string.IsNullOrEmpty(#m.ReportData2.DisplayFormat)) ? Convert.ToDecimal(#m.ReportData2.DecimalValue).ToString("N") : Convert.ToDecimal(#m.ReportData2.DecimalValue).ToString(#m.ReportData1.DisplayFormat));
if (items[0].ReportData2.SumValue || items[0].ReportData2.AvgValue)
{
displaySummary = true;
SummaryData.Add(
new ReportDataDataContract
{
Name = items[0].ReportData2.Name,
AvgValue = items[0].ReportData2.AvgValue,
DecimalValue = 0
});
}
}
else if (items[0].ReportData2.IntValue != null)
{
Columns.Add(m => m.ReportData2.IntValue).Titled(items[0].ReportData2.Name);
if (items[0].ReportData2.SumValue || items[0].ReportData2.AvgValue)
{
displaySummary = true;
SummaryData.Add(
new ReportDataDataContract
{
Name = items[0].ReportData2.Name,
AvgValue = items[0].ReportData2.AvgValue,
IntValue = 0
});
}
}
else
{
Columns.Add(m => m.ReportData2.StringValue).Titled(items[0].ReportData2.Name);
}
}
This method consists of code that repeats itself out to ReportData6, changing only the ReportData field name with each repetition. Is there a way that this method can be rewritten to process each ReportData field by looping somehow? Besides making for a shorter method, this would be extremely useful to have in order to avoid manually updating the method if additional ReportData fields need to be added to the ReportRowDataContract class in the future.
Edit #1: I am fairly new to C# so detailed answers of how to go about this would be immensely helpful.
Edit #2: Thanks to Zohar Peled's post below, the following code feels very close. However, m.ReportData1 is causing problems in the AddGridColumn() method. The error message is 'ReportRowDataContract' does not contain a definition for 'item'...
I tried passing in ReportData 1 as a second argument when AddGridColumn() is called, but to no avail. Is there a way to modify the code so it works?
code that calls method:
// create columns for grid
AddGridColumn(items[0].ReportData1);
AddGridColumn(items[0].ReportData2);
AddGridColumn(items[0].ReportData3);
AddGridColumn(items[0].ReportData4);
AddGridColumn(items[0].ReportData5);
AddGridColumn(items[0].ReportData6);
method:
private void AddGridColumn(ReportDataDataContract item)
{
if (item != null)
{
if (item.DecimalValue != null)
{
Columns.Add(m => m.ReportData1.DecimalValue).Titled(item.Name).Encoded(false).
Sanitized(false).RenderValueAs(
m => (string.IsNullOrEmpty(#m.ReportData1.DisplayFormat)) ?
Convert.ToDecimal(#m.ReportData1.DecimalValue).ToString("N") :
Convert.ToDecimal(#m.ReportData1.DecimalValue).ToString(#m.ReportData1.DisplayFormat));
if (item.SumValue || item.AvgValue)
{
displaySummary = true;
SummaryData.Add(
new ReportDataDataContract
{
Name = item.Name,
AvgValue = item.AvgValue,
DecimalValue = 0
});
}
}
else if (item.IntValue != null)
{
Columns.Add(m => m.ReportData1.IntValue).Titled(item.Name);
if (item.SumValue || item.AvgValue)
{
displaySummary = true;
SummaryData.Add(
new ReportDataDataContract
{
Name = item.Name,
AvgValue = item.AvgValue,
IntValue = 0
});
}
}
else
{
Columns.Add(m => m.ReportData1.StringValue).Titled(item.Name);
}
}
}
Edit #3: This is the ReportDataDataContract class definiton:
public class ReportDataDataContract
{
public string Name { get; set; }
public string StringValue { get; set; }
public decimal? DecimalValue { get; set; }
public int? IntValue { get; set; }
public bool SumValue { get; set; }
public bool AvgValue { get; set; }
public int? Index { get; set; }
public string DisplayFormat { get; set; }
}
Can you add an indexed property like this to ReportRowDataContract in order to essentially access the ReportData values as an indexed property?
public ReportDataDataContract this[int i]
{
get
{
return new ReportDataDataContract[] { ReportData1, ReportData2, ReportData3, ReportData4, ReportData5, ReportData6 }[i];
}
}
The you can use items[0][i].SumValue, for example, instead of items[0].ReportData1.SumValue.
If you can, you should simply change the ReportRowDataContract to only hold a single property that is an array of ReportDataDataContract. Then you can use a simple loop for each of the data contracts.
If you can't change the ReportRowDataContract table, you could use a method that takes in an argument of type ReportDataDataContract and have all the repeated code there - and just call it one time for each property.
The method should look something like that (based on the code you've posted):
void DoSomething(ReportDataDataContract dataContranct)
{
if (dataContranct != null)
{
if (dataContranct.DecimalValue != null)
{
Columns.Add(m => dataContranct.DecimalValue).Titled(dataContranct.Name).Encoded(false).
Sanitized(false).RenderValueAs(
m => (string.IsNullOrEmpty(#dataContranct.DisplayFormat)) ?
Convert.ToDecimal(#dataContranct.DecimalValue).ToString("N") :
Convert.ToDecimal(#dataContranct.DecimalValue).ToString(#dataContranct.DisplayFormat));
if (dataContranct.SumValue || dataContranct.AvgValue)
{
displaySummary = true;
SummaryData.Add(
new ReportDataDataContract
{
Name = dataContranct.Name,
AvgValue = dataContranct.AvgValue,
DecimalValue = 0
});
}
}
else if (dataContranct.IntValue != null)
{
Columns.Add(m => dataContranct.IntValue).Titled(dataContranct.Name);
if (dataContranct.SumValue || dataContranct.AvgValue)
{
displaySummary = true;
SummaryData.Add(
new ReportDataDataContract
{
Name = dataContranct.Name,
AvgValue = dataContranct.AvgValue,
IntValue = 0
});
}
}
else
{
Columns.Add(m => mdataContranct.StringValue).Titled(dataContranct.Name);
}
}
}
Before deserialize existing .proto file i need to check whether the array property (HtmlCleanerEngineProfile[]) has already satisfied condition. If not, then i should recreate the property.
And here's the problem, after HtmlCleanerEngineProfile[] has been recreated (inside the constructor) this new property still has same value (and length) with old property.
[ProtoContract]
public class HtmlCleanerTemplate : ModelBase
{
private string _templateName;
private int _recursiveLevel = 3;
[ProtoMember(1)]
public string TemplateName
{
get => _templateName;
set => SetProperty(ref _templateName, value);
}
[ProtoMember(2), DefaultValue(3)]
public int RecursiveLevel
{
get => _recursiveLevel;
set => SetProperty(ref _recursiveLevel, value);
}
// This one..
[ProtoMember(3, OverwriteList = true)]
public HtmlCleanerEngineProfile[] EngineProfiles { get; private set; }
public HtmlCleanerTemplate()
{
var engineTypes = RetrieveEngineTypes();
if (EngineProfiles != null && EngineProfiles.Length == engineTypes.Count) return;
// 1. Clone existing to restore checked state
// 2. Recreate HtmlCleanerEngineProfile[]
var tempProfiles = EngineProfiles?.Clone() as HtmlCleanerEngineProfile[];
EngineProfiles = new HtmlCleanerEngineProfile[engineTypes.Count];
for (var i = 0; i < engineTypes.Count; i++)
{
EngineProfiles[i] = new HtmlCleanerEngineProfile
{
EngineName = engineTypes[i].Name,
EngineDescription = ReflectionUtils.GetDescription(engineTypes[i]),
};
// Restore checked state
if (tempProfiles != null && i < tempProfiles.Length)
{
// Todo: if (EngineProfiles[i].EngineName == tempEngines[i].EngineName)
EngineProfiles[i].EngineChecked = tempProfiles[i].EngineChecked;
}
}
}
private static IList<Type> RetrieveEngineTypes()
{
return ReflectionUtils
.GetTypes("ContentManager.Core.Document.Cleaner")
.Where(x => typeof(IHtmlCleanerEngine).IsAssignableFrom(x) && x.Name != typeof(IHtmlCleanerEngine).Name)
.ToList();
}
}
And the HtmlCleanerEnglineProfile
[ProtoContract]
public sealed class HtmlCleanerEngineProfile
{
internal HtmlCleanerEngineProfile() { }
[ProtoMember(1)]
public string EngineName { get; set; }
[ProtoMember(2)]
public string EngineDescription { get; set; }
[ProtoMember(3)]
public bool EngineChecked { get; set; }
}
I've trying to create a new method (same as code inside constructor) and assign [ProtoBeforeDeserialization] attribute. But still i got same result. Did I do something wrong?
Solved by adding [ProtoAfterDeserialization] attribute and [ProtoContract(SkipConstructor = true)].
[ProtoContract(SkipConstructor = true)]
public class HtmlCleanerTemplate : ModelBase
New method
[ProtoAfterDeserialization]
protected void AfterDeserialization()
{
var engineTypes = RetrieveEngineTypes();
if (EngineProfiles != null && EngineProfiles.Length == engineTypes.Count) return;
.......
}
Ctor..
public HtmlCleanerTemplate()
{
AfterDeserialization();
}
IDataErrorInfo: Validating when page submitted
Here I am using IdataErrorInfo
Below is My MainwindowViewModel Class COde
using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using MahApps.Metro;
using MetroDemo.Models;
using System.Windows.Input;
using MahApps.Metro.Controls;
using MahApps.Metro.Controls.Dialogs;
namespace MetroDemo
{
public class AccentColorMenuData
{
public string Name { get; set; }
public Brush BorderColorBrush { get; set; }
public Brush ColorBrush { get; set; }
private ICommand changeAccentCommand;
public ICommand ChangeAccentCommand
{
get { return this.changeAccentCommand ?? (changeAccentCommand = new SimpleCommand { CanExecuteDelegate = x => true, ExecuteDelegate = x => this.DoChangeTheme(x) }); }
}
protected virtual void DoChangeTheme(object sender)
{
var theme = ThemeManager.DetectAppStyle(Application.Current);
var accent = ThemeManager.GetAccent(this.Name);
ThemeManager.ChangeAppStyle(Application.Current, accent, theme.Item1);
}
}
public class AppThemeMenuData : AccentColorMenuData
{
protected override void DoChangeTheme(object sender)
{
var theme = ThemeManager.DetectAppStyle(Application.Current);
var appTheme = ThemeManager.GetAppTheme(this.Name);
ThemeManager.ChangeAppStyle(Application.Current, theme.Item2, appTheme);
}
}
public class MainWindowViewModel : INotifyPropertyChanged, IDataErrorInfo
{
private readonly IDialogCoordinator _dialogCoordinator;
int? _integerGreater10Property;
int? _emptystring;
private bool _animateOnPositionChange = true;
public MainWindowViewModel(IDialogCoordinator dialogCoordinator)
{
_dialogCoordinator = dialogCoordinator;
SampleData.Seed();
// create accent color menu items for the demo
this.AccentColors = ThemeManager.Accents
.Select(a => new AccentColorMenuData() { Name = a.Name, ColorBrush = a.Resources["AccentColorBrush"] as Brush })
.ToList();
// create metro theme color menu items for the demo
this.AppThemes = ThemeManager.AppThemes
.Select(a => new AppThemeMenuData() { Name = a.Name, BorderColorBrush = a.Resources["BlackColorBrush"] as Brush, ColorBrush = a.Resources["WhiteColorBrush"] as Brush })
.ToList();
Albums = SampleData.Albums;
Artists = SampleData.Artists;
FlipViewTemplateSelector = new RandomDataTemplateSelector();
FrameworkElementFactory spFactory = new FrameworkElementFactory(typeof(Image));
spFactory.SetBinding(Image.SourceProperty, new System.Windows.Data.Binding("."));
spFactory.SetValue(Image.HorizontalAlignmentProperty, HorizontalAlignment.Stretch);
spFactory.SetValue(Image.StretchProperty, Stretch.Fill);
FlipViewTemplateSelector.TemplateOne = new DataTemplate()
{
VisualTree = spFactory
};
FlipViewImages = new string[] { "http://trinities.org/blog/wp-content/uploads/red-ball.jpg", "http://savingwithsisters.files.wordpress.com/2012/05/ball.gif" };
RaisePropertyChanged("FlipViewTemplateSelector");
BrushResources = FindBrushResources();
CultureInfos = CultureInfo.GetCultures(CultureTypes.InstalledWin32Cultures).ToList();
}
public string Title { get; set; }
public int SelectedIndex { get; set; }
public List<Album> Albums { get; set; }
public List<Artist> Artists { get; set; }
public List<AccentColorMenuData> AccentColors { get; set; }
public List<AppThemeMenuData> AppThemes { get; set; }
public List<CultureInfo> CultureInfos { get; set; }
public int? IntegerGreater10Property
{
get { return this._integerGreater10Property; }
set
{
if (Equals(value, _integerGreater10Property))
{
return;
}
_integerGreater10Property = value;
RaisePropertyChanged("IntegerGreater10Property");
}
}
public string FirstName { get; set; }
public string LastName { get; set; }
DateTime? _datePickerDate;
public DateTime? DatePickerDate
{
get { return this._datePickerDate; }
set
{
if (Equals(value, _datePickerDate))
{
return;
}
_datePickerDate = value;
RaisePropertyChanged("DatePickerDate");
}
}
bool _magicToggleButtonIsChecked = true;
public bool MagicToggleButtonIsChecked
{
get { return this._magicToggleButtonIsChecked; }
set
{
if (Equals(value, _magicToggleButtonIsChecked))
{
return;
}
_magicToggleButtonIsChecked = value;
RaisePropertyChanged("MagicToggleButtonIsChecked");
}
}
private bool _quitConfirmationEnabled;
public bool QuitConfirmationEnabled
{
get { return _quitConfirmationEnabled; }
set
{
if (value.Equals(_quitConfirmationEnabled)) return;
_quitConfirmationEnabled = value;
RaisePropertyChanged("QuitConfirmationEnabled");
}
}
}
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises the PropertyChanged event if needed.
/// </summary>
/// <param name="propertyName">The name of the property that changed.</param>
protected virtual void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public string this[string columnName]
{
get
{
if (columnName == "IntegerGreater10Property" && this.IntegerGreater10Property < 10)
{
return "Number is not greater than 10!";
}
if (columnName == "DatePickerDate" && this.DatePickerDate == null)
{
return "No Date given!";
}
if (columnName == "FirstName")
{
if (string.IsNullOrEmpty(FirstName) || FirstName.Length < 3)
return "Please Enter First Name";
}
if (columnName == "LastName")
{
if (string.IsNullOrEmpty(FirstName) || FirstName.Length < 3)
return "Please Enter Last Name";
}
return null;
}
}
public string Error { get { return string.Empty; } }
private ICommand singleCloseTabCommand;
public ICommand SingleCloseTabCommand
{
get
{
return this.singleCloseTabCommand ?? (this.singleCloseTabCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = async x =>
{
await ((MetroWindow) Application.Current.MainWindow).ShowMessageAsync("Closing tab!", string.Format("You are now closing the '{0}' tab", x));
}
});
}
}
private ICommand neverCloseTabCommand;
public ICommand NeverCloseTabCommand
{
get { return this.neverCloseTabCommand ?? (this.neverCloseTabCommand = new SimpleCommand { CanExecuteDelegate = x => false }); }
}
private ICommand showInputDialogCommand;
public ICommand ShowInputDialogCommand
{
get
{
return this.showInputDialogCommand ?? (this.showInputDialogCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x =>
{
_dialogCoordinator.ShowInputAsync(this, "From a VM", "This dialog was shown from a VM, without knowledge of Window").ContinueWith(t => Console.WriteLine(t.Result));
}
});
}
}
private ICommand showProgressDialogCommand;
public ICommand ShowProgressDialogCommand
{
get
{
return this.showProgressDialogCommand ?? (this.showProgressDialogCommand = new SimpleCommand
{
CanExecuteDelegate = x => true,
ExecuteDelegate = x => RunProgressFromVm()
});
}
}
}
}
Below is My XAml Code
<TextBox Name="txt_LA_FirstName"
Controls:TextBoxHelper.Watermark="First Name"
Controls:TextBoxHelper.UseFloatingWatermark="True"
ToolTip="First Name" Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Stretch" Text="{Binding FirstName, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged, NotifyOnValidationError=True}" />
Event Is Raising at form load and showing Error Messages Before user enters any Value> I need to show Error Messages After Button Click Event
The reason why you get it is because the content of your indexer is static, so the View validates it during the binding. You should only validate it when the property changes though.
You should have a backing field for an errors dictionary with Add/Remove methods, like this
private readonly Dictionary<string, string> errors = new Dictionary<string, string>();
public string this[string columnName]
{
get
{
string errorMessage = null;
if (errors.TryGetValue(columnName, errorMessage)
{
return errorMessage;
}
return null;
}
}
protected void AddErrorMessage(string columnName, string errorMessage)
{
errors[columnName] = errorMessage;
}
protected void RemoveErrorMessage(string columnName)
{
if(errors.ContainsKey(columnName))
{
errors.Remove(columnName);
}
}
protected virtual void Validate()
{
errors.Clear();
if (this.IntegerGreater10Property < 10)
{
this.AddErrorMessage("IntegerGreater10Property", "Number is not greater than 10!");
}
if (this.DatePickerDate == null)
{
this.AddErrorMessage("DatePickerDate", "No Date given!");
}
if (string.IsNullOrEmpty(FirstName) || FirstName.Length < 3)
{
this.AddErrorMessage("FirstName", "Please Enter First Name");
}
if (string.IsNullOrEmpty(LastName) || Lastname.Length < 3)
{
this.AddErrorMessage("LastName", "Please Enter Last Name");
}
}
And then remove them with RemoveMessage("FirstName") or clear it up on validation. You should implement this in a base class of course, to avoid repeating this code over and over again and just override the Validate() method in your ViewModel classes
Thanks for your reply
Even I tried it I had solved it By creating on Bool variable and declaring it as False in MainwindowView model class.
Unless and Until the Bool Variable is True it wont Validates
On Button click Event I will Enable the bool Variable to True then It starts Validates on Button CLick
if (Enable)
{
if (columnName == "IntegerGreater10Property" && this.IntegerGreater10Property < 10)
{
return "Number is not greater than 10!";
}
if (columnName == "DatePickerDate" && this.DatePickerDate == null)
{
return "No Date given!";
}
if (columnName == "FirstName")
{
if (string.IsNullOrEmpty(FirstName) || FirstName.Length < 3)
return "Please Enter First Name";
}
if (columnName == "LastName")
{
if (string.IsNullOrEmpty(FirstName) || FirstName.Length < 3)
return "Please Enter Last Name";
}
}
return null;
Below is My Button click Event (On Button I will Enable the Bool Property to true
var x = (MainWindowViewModel)DataContext;
x.Enable = true;
var binding = txt_LA_FirstName.GetBindingExpression(TextBox.TextProperty);
binding.UpdateSource();