I'm looking to create a slick helper function that compares two values, returns true if they are different, and also updates the first value to equal the second value. This is what I came up with:
bool UpdateHelper<T>(ref T originalProperty, T newProperty) =>
!Equals(originalProperty, newProperty) && (originalProperty = newProperty) is T _;
bool changes = false;
changes |= UpdateHelper(ref name, frmName.Text);
changes |= UpdateHelper(ref description, frmDescription.Text);
...
The goal is that when doing this for dozens of properties, to avoid copy-pasting the same 5 lines of code for each property:
if (myModel.SomeProperty != someUserInput.Text)
{
myModel.SomeProperty = someUserInput.Text
changes = true
}
(Especially after having seen someone copy paste and update the if statement portion but forgetting to update the assignment!)
My above helper works for for the most part, but unfortunately this falls apart when I try to use it on properties:
changes |= UpdateHelper(ref myModel.Name, frmName.Text);
changes |= UpdateHelper(ref myModel.Description, frmDescription.Text);
Property access returns temporary value. 'ref' argument must be an assignable variable, field or an array element
Can anyone think of a solution that would work for both?
I tried playing around with expressions, e.g.:
bool UpdateHelper<T>(Expression<Func<T>> originalProperty, T newProperty) =>
!Equals(originalProperty.Compile().Invoke(), newProperty) && (originalProperty.Body.??? = newProperty) is T _;
changes |= UpdateHelper(() => myModel.Name, frmName.Text);
changes |= UpdateHelper(() => myModel.Description, frmDescription.Text);
changes |= UpdateHelper(() => myModel.ChildObject.Name, frmChildName.Text);
but I feel like it's going to turn into a nightmare of parsing expression trees, especially if it needs to handle a special case of accessing properties nested a few layers down (like the last example above).
Anyone ideas for how to do this elegantly?
CodePudding user response:
Expressions are slow, as you see in your code you need to compile them for every assignment. One solution if you want to support both cases is to provide separate functions for getting and setting:
bool UpdateHelper<T>(Func<T> originalGet, Action<T> originalSet, T newProperty) => ...
And you'd call it like (for both fields and properties):
changes |= UpdateHelper(() => name, val => name = val, frmName.Text);
Note that the most elegant solution involves never writing this at all, but having it source-generated, either with the template compiler (.tt
) or with plain source generators (ISourceGenerator
/ IIncrementalGenerator
).
CodePudding user response:
I think I've got it solved. This sort of expression parsing is relatively common when handling e.g. property changed event notifications.
bool UpdateHelper<T>(Expression<Func<T>> originalProperty, T newProperty)
{
if (Equals(originalProperty.Compile().DynamicInvoke(), newProperty))
return false;
if (originalProperty.Body is MemberExpression mex && mex.Member is PropertyInfo pi)
pi.SetValue(Expression.Lambda(mex.Expression).Compile().DynamicInvoke(), newProperty);
else
throw new ArgumentException("The originalProperty argument must be a property expression such as `() => someObject.SomeProperty`");
return true;
}