Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sort the bindings in invalid command invocation error #1714

Merged
merged 1 commit into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 102 additions & 43 deletions src/Framework/Framework/Runtime/Commands/EventValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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, CandidateBindings>();
string? errorMessage = null;
var candidateBindings = new Dictionary<FindBindingResult.BindingMatchChecklist, CandidateBindings>();
var infoMessage = new StringBuilder();

var walker = new ControlTreeWalker(viewRootControl);
walker.ProcessControlTree((control) =>
Expand All @@ -74,84 +72,98 @@ private FindBindingResult FindCommandBinding(string[] path, string commandId,

foreach (var binding in bindings)
{
var wrongExceptionPropertyKeys = new List<string>();
var correctExceptionPropertyKeys = new List<string>();
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<string, IBinding>(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,
Expand Down Expand Up @@ -191,17 +203,64 @@ private FindBindingResult FindControlCommandBinding(string[] path, string comman
/// <summary>
/// Throws the event validation exception.
/// </summary>
private InvalidCommandInvocationException EventValidationException(string? errorMessage = null, Dictionary<string, CandidateBindings>? data = null)
=> new InvalidCommandInvocationException(errorMessage == null ? "Invalid command invocation!" : errorMessage, data);
private InvalidCommandInvocationException EventValidationException(string? errorMessage = null, Dictionary<FindBindingResult.BindingMatchChecklist, CandidateBindings>? data = null)
{
var stringifiedData =
data?.OrderByDescending(k => (k.Key.BindingIdMatch, k.Key.ControlMatch, -k.Key.MismatchCount))
.Select(k => new KeyValuePair<string, string[]>(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<string, CandidateBindings> CandidateBindings { get; set; } = new();
public Dictionary<BindingMatchChecklist, CandidateBindings> CandidateBindings { get; set; } = new();

public CommandBindingExpression? Binding { get; set; }
public DotvvmBindableObject? Control { get; set; }
public DotvvmProperty? Property { get; set; }

public struct BindingMatchChecklist: IEquatable<BindingMatchChecklist>
{
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 : "");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace DotVVM.Framework.Runtime.Commands
{
public class InvalidCommandInvocationException : Exception
{
public Dictionary<string, string[]>? AdditionData { get; set; }
public KeyValuePair<string, string[]>[]? AdditionData { get; set; }

public InvalidCommandInvocationException(string message)
: base(message)
Expand All @@ -19,24 +19,16 @@ public InvalidCommandInvocationException(string message, Exception innerExceptio

}

public InvalidCommandInvocationException(string message, Dictionary<string, CandidateBindings>? data)
public InvalidCommandInvocationException(string message, KeyValuePair<string, string[]>[]? data)
: this(message, (Exception?)null, data)
{

}

public InvalidCommandInvocationException(string message, Exception? innerException, Dictionary<string, CandidateBindings>? data)
public InvalidCommandInvocationException(string message, Exception? innerException, KeyValuePair<string, string[]>[]? data)
: base(message, innerException)
{
if(data != null)
{
AdditionData = new Dictionary<string, string[]>();
foreach (var bindings in data)
{
AdditionData.Add(bindings.Key, bindings.Value.BindingsToString());
}
}

AdditionData = data;
}
}
}
Loading