diff --git a/src/Framework/Framework/Runtime/Commands/EventValidator.cs b/src/Framework/Framework/Runtime/Commands/EventValidator.cs index fbadc33685..f91467d556 100644 --- a/src/Framework/Framework/Runtime/Commands/EventValidator.cs +++ b/src/Framework/Framework/Runtime/Commands/EventValidator.cs @@ -58,10 +58,8 @@ private FindBindingResult FindCommandBinding(string[] path, string commandId, DotvvmBindableObject? resultControl = null; DotvvmProperty? resultProperty = null; - bool checkControl; - bool bindingInPath = false; - var candidateBindings = new Dictionary(); - string? errorMessage = null; + var candidateBindings = new Dictionary(); + var infoMessage = new StringBuilder(); var walker = new ControlTreeWalker(viewRootControl); walker.ProcessControlTree((control) => @@ -74,84 +72,98 @@ private FindBindingResult FindCommandBinding(string[] path, string commandId, foreach (var binding in bindings) { - var wrongExceptionPropertyKeys = new List(); - var correctExceptionPropertyKeys = new List(); - var infoMessage = new StringBuilder(); + infoMessage.Clear(); + var bindingMatch = new FindBindingResult.BindingMatchChecklist(); // checking path if (!ViewModelPathComparer.AreEqual(path, walker.CurrentPathArray)) { - wrongExceptionPropertyKeys.Add("DataContext path"); infoMessage.Append( $"Expected DataContext path: '{string.Join("/", path)}' Command binding DataContext path: '{string.Join("/", walker.CurrentPathArray)}'"); } else { - //Found a binding in DataContext - bindingInPath = true; - correctExceptionPropertyKeys.Add("DataContext path"); + bindingMatch.DataContextPathMatch = true; } //checking binding id - if (((CommandBindingExpression) binding.Value).BindingId != commandId) - { - wrongExceptionPropertyKeys.Add("binding id"); - } - else + if (((CommandBindingExpression) binding.Value).BindingId == commandId) { - correctExceptionPropertyKeys.Add("binding id"); + bindingMatch.BindingIdMatch = true; } //checking validation path var currentValidationTargetPath = KnockoutHelper.GetValidationTargetExpression(control)?.identificationExpression; if (currentValidationTargetPath != validationTargetPath) { - wrongExceptionPropertyKeys.Add("validation path"); - infoMessage.Append($"Expected validation path: '{string.Join("/", validationTargetPath)}' Command binding validation path: '{string.Join("/", currentValidationTargetPath)}'"); + if (infoMessage.Length > 0) + infoMessage.Append("; "); + infoMessage.Append($"Expected validation path: '{validationTargetPath}' Command binding validation path: '{currentValidationTargetPath}'"); } else { - correctExceptionPropertyKeys.Add("validation path"); + bindingMatch.ValidationPathMatch = true; } //If finding control command binding checks if the binding is control otherwise always true - checkControl = !findControl || control.GetClosestControlBindingTarget() == targetControl; + bindingMatch.ControlMatch = !findControl || control.GetClosestControlBindingTarget() == targetControl; + + if (!bindingMatch.ControlMatch) + { + if (infoMessage.Length > 0) + { + infoMessage.Append("; different markup control"); + } + else + { + infoMessage.Append($"Expected control: '{(targetControl as DotvvmControl)?.GetDotvvmUniqueId()}' Command binding control: '{(control.GetClosestControlBindingTarget() as DotvvmControl)?.GetDotvvmUniqueId()}'"); + } + } - if(!wrongExceptionPropertyKeys.Any() && checkControl) + if(bindingMatch.AllMatches) { //correct binding found resultBinding = (CommandBindingExpression)binding.Value; resultControl = control; resultProperty = binding.Key; } - else if (wrongExceptionPropertyKeys.Any()) + else { - var exceptionPropertyKey = - (findControl && checkControl - ? "Control command bindings with wrong " - : "Command bindings with wrong ") + string.Join(", ", wrongExceptionPropertyKeys) - + (correctExceptionPropertyKeys.Any() - ? " and correct " + string.Join(", ", correctExceptionPropertyKeys) - : "") - + ":"; - if (!candidateBindings.ContainsKey(exceptionPropertyKey)) + // only add information about ID mismatch if no other mismatch was found to avoid information clutter + if (!bindingMatch.BindingIdMatch && infoMessage.Length == 0) { - candidateBindings.Add(exceptionPropertyKey, new CandidateBindings()); + infoMessage.Append($"Expected internal binding id: '{commandId}' Command binding id: '{((CommandBindingExpression)binding.Value).BindingId}'"); } - candidateBindings[exceptionPropertyKey] - .AddBinding(new KeyValuePair(infoMessage.ToString(), binding.Value)); - } - else - { - errorMessage = "Invalid command invocation (the binding is not control command binding)"; + if (!candidateBindings.ContainsKey(bindingMatch)) + { + candidateBindings.Add(bindingMatch, new CandidateBindings()); + } + candidateBindings[bindingMatch] + .AddBinding(new(infoMessage.ToString(), binding.Value)); } } } }); + string? errorMessage = null; + if (candidateBindings.ContainsKey(new FindBindingResult.BindingMatchChecklist { ControlMatch = false, BindingIdMatch = true, DataContextPathMatch = true, ValidationPathMatch = true })) + { + // all properties match except the control + errorMessage = "Invalid command invocation - The binding is not control command binding."; + } + else if (candidateBindings.All(b => !b.Key.DataContextPathMatch)) + { + // nothing in the specified data context path + errorMessage = $"Invalid command invocation - Nothing was found inside DataContext '{path}'. Please check if ViewModel is populated."; + } + else + { + errorMessage = "Invalid command invocation - The specified command binding was not found."; + } + return new FindBindingResult { - ErrorMessage = bindingInPath ? errorMessage : "Nothing was found inside specified DataContext. Please check if ViewModel is populated.", + ErrorMessage = errorMessage, CandidateBindings = candidateBindings, Binding = resultBinding, Control = resultControl, @@ -191,17 +203,64 @@ private FindBindingResult FindControlCommandBinding(string[] path, string comman /// /// Throws the event validation exception. /// - private InvalidCommandInvocationException EventValidationException(string? errorMessage = null, Dictionary? data = null) - => new InvalidCommandInvocationException(errorMessage == null ? "Invalid command invocation!" : errorMessage, data); + private InvalidCommandInvocationException EventValidationException(string? errorMessage = null, Dictionary? data = null) + { + var stringifiedData = + data?.OrderByDescending(k => (k.Key.BindingIdMatch, k.Key.ControlMatch, -k.Key.MismatchCount)) + .Select(k => new KeyValuePair(k.Key.ToString(), k.Value.BindingsToString())) + .ToArray(); + return new InvalidCommandInvocationException(errorMessage ?? "Invalid command invocation!", stringifiedData); + } } public class FindBindingResult { public string? ErrorMessage { get; set; } - public Dictionary CandidateBindings { get; set; } = new(); + public Dictionary CandidateBindings { get; set; } = new(); public CommandBindingExpression? Binding { get; set; } public DotvvmBindableObject? Control { get; set; } public DotvvmProperty? Property { get; set; } + + public struct BindingMatchChecklist: IEquatable + { + public bool ValidationPathMatch { get; set; } + public bool BindingIdMatch { get; set; } + public bool DataContextPathMatch { get; set; } + public bool ControlMatch { get; set; } + + public readonly bool AllMatches => ValidationPathMatch && BindingIdMatch && DataContextPathMatch && ControlMatch; + public readonly int MismatchCount => (ValidationPathMatch ? 0 : 1) + (BindingIdMatch ? 0 : 1) + (DataContextPathMatch ? 0 : 1) + (ControlMatch ? 0 : 1); + + public readonly bool Equals(BindingMatchChecklist other) => ValidationPathMatch == other.ValidationPathMatch && BindingIdMatch == other.BindingIdMatch && DataContextPathMatch == other.DataContextPathMatch && ControlMatch == other.ControlMatch; + public readonly override bool Equals(object? obj) => obj is BindingMatchChecklist other && Equals(other); + public readonly override int GetHashCode() => (ValidationPathMatch, BindingIdMatch, DataContextPathMatch, ControlMatch).GetHashCode(); + + public readonly override string ToString() + { + if (AllMatches) + { + return "Matching binding"; + } + if (!ControlMatch && ValidationPathMatch && BindingIdMatch && DataContextPathMatch) + { + return "The binding is not control command binding or is in wrong control"; + } + if (!ValidationPathMatch && !BindingIdMatch && !DataContextPathMatch) + { + return "No matching property"; + } + var properties = new [] { + ("binding id", this.BindingIdMatch), + ("validation path", this.ValidationPathMatch), + ("DataContext path", this.DataContextPathMatch) + }.ToLookup(p => p.Item2, p => p.Item1); + + var wrong = string.Join(", ", properties[false]); + var correct = string.Join(", ", properties[true]); + + return "Command binding with wrong " + wrong + (correct.Any() ? " and correct " + correct : ""); + } + } } } diff --git a/src/Framework/Framework/Runtime/Commands/InvalidCommandInvocationException.cs b/src/Framework/Framework/Runtime/Commands/InvalidCommandInvocationException.cs index 84085d3f47..93993aa3ad 100644 --- a/src/Framework/Framework/Runtime/Commands/InvalidCommandInvocationException.cs +++ b/src/Framework/Framework/Runtime/Commands/InvalidCommandInvocationException.cs @@ -5,7 +5,7 @@ namespace DotVVM.Framework.Runtime.Commands { public class InvalidCommandInvocationException : Exception { - public Dictionary? AdditionData { get; set; } + public KeyValuePair[]? AdditionData { get; set; } public InvalidCommandInvocationException(string message) : base(message) @@ -19,24 +19,16 @@ public InvalidCommandInvocationException(string message, Exception innerExceptio } - public InvalidCommandInvocationException(string message, Dictionary? data) + public InvalidCommandInvocationException(string message, KeyValuePair[]? data) : this(message, (Exception?)null, data) { } - public InvalidCommandInvocationException(string message, Exception? innerException, Dictionary? data) + public InvalidCommandInvocationException(string message, Exception? innerException, KeyValuePair[]? data) : base(message, innerException) { - if(data != null) - { - AdditionData = new Dictionary(); - foreach (var bindings in data) - { - AdditionData.Add(bindings.Key, bindings.Value.BindingsToString()); - } - } - + AdditionData = data; } } }