Introduction
EditableObject
is designed as an advanced view model property editor. It encapsulates dirty/edited/originalValue etc.. functions within properties of Model(TObj). So that binding EditableProperty
in UI can easily notify when value is changed, or rollback to original value, or save value.
This object use Linq Expression and Reflection to get or set values of an object.
While I'm writing this, my c# version has no support fornameof
keyword, so I have to use Linq Expression instead, or there will be a more flexible and clear codes.
Usage ViewModel
public partial class EditableObjectTest : ViewModelBase
{
public EditableObject EditablePerson { get; }
public EditableProperty EditableFirstName => EditablePerson[() => Model.FirstName];
public EditableProperty EditableLastName => EditablePerson[() => Model.LastName];
public EditableProperty EditableAddress => EditablePerson[() => Model.Address];
public Person Model { get; set; }
public EditableObjectTest()
{
Model = new Person {FirstName = "Z", LastName = "w", Address = "1003No"};
EditablePerson = new EditableObject(Model,
new Expression>[] //System.Linq.Expressions.Expression
{
() => Model.FirstName,
() => Model.LastName,
() => Model.Address,
} );
EditablePerson.PropertyChanged += property => { };
}
}
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }
}
Usage of UI
Object Structure
public class EditableObject
{
private bool _canMarkDirty;
public event Action> PropertyChanged;
public TObj SourceObject { get; }
public Dictionary> PropertyDictionary { get; }
///
/// A flag that determines whether each editable property can be marked as modified or not if value is changed.
///
public bool CanMarkDirty
{
get { return _canMarkDirty; }
set
{
_canMarkDirty = value;
foreach (var prop in PropertyDictionary.Values)
{
prop.CanMarkDirty = value;
}
}
}
public EditableObject(TObj sourceObj, IEnumerable>> propExpressions)
{
SourceObject = sourceObj;
PropertyDictionary = new Dictionary>();
foreach (var propExp in propExpressions)
{
var propInfo = ExtractExpressionToPropInfo(propExp);
if (propInfo == null)
{
continue;
}
var key = propInfo.Name;
var getValueFunc = propExp.Compile();
var value = getValueFunc();
var newProp = new EditableProperty(value);
newProp.ValueChanged += sender =>
{
SetEditedValue(sender, sourceObj);
PropertyChanged?.Invoke(sender);
};
PropertyDictionary.Add(key, newProp);
}
}
///
/// Checks if there're properties that are modified.
///
public bool ContainsModifications()
{
return PropertyDictionary.Values.Any(o => o.IsDirty);
}
///
/// Sets modified property value to to specific object.
///
public void SetEditedValue(EditableProperty editableProp, TObj targetObj)
{
var key = PropertyDictionary.First(p => p.Value == editableProp).Key;
typeof(TObj).GetProperty(key).SetValue(targetObj, editableProp.Value);
}
///
/// Sets all modified property values to to specific object.
///
public void SetEditedValuesTo(TObj targetObj)
{
foreach (var prop in PropertyDictionary.Values)
{
SetEditedValue(prop, targetObj);
}
}
///
/// Saves all s' values (including setting original value and reset modified flag).
///
public void SaveChanges()
{
foreach (var prop in PropertyDictionary.Values)
{
prop.SaveValue();
}
}
///
/// Discards all s' values (including setting original value and reset modified flag).
///
public void DiscardChanges()
{
foreach (var prop in PropertyDictionary.Values)
{
prop.ResetValue();
}
}
///
/// Extracts from property expression.
///
private PropertyInfo ExtractExpressionToPropInfo(Expression> propExpression)
{
var propInfo = (propExpression.Body as MemberExpression)?.Member as PropertyInfo;
if (propInfo == null)
{
Console.WriteLine("EditableObjectCollection ExtractExpressionToPropInfo failed: Cannot resolve expression: {0}", propExpression.Name);
}
return propInfo;
}
///
/// The Indexer that gets via specific property expression.
///
public EditableProperty this[Expression> propExpression] => PropertyDictionary[ExtractExpressionToPropInfo(propExpression).Name];
}
public class EditableProperty : ObservableObject
{
private T _value;
private T _originalValue;
private bool _isDirty;
private bool _canMarkDirty;
public event Action> ValueChanged;
///
/// Please use SetValue method instead of Property-Setter.
///
public T Value
{
get { return _value; }
set
{
_value = value;
RaisePropertyChanged(() => Value);
if (CanMarkDirty)
{
//special case: string.Empty and null is regarded as same.
if (typeof(T) == typeof(string) &&
string.IsNullOrEmpty(Value as string) &&
string.IsNullOrEmpty(OriginalValue as string))
{
IsDirty = false;
}
else
{
IsDirty = !Equals(Value, OriginalValue);
}
}
ValueChanged?.Invoke(this);
}
}
///
/// The original value for value modification comparison.
///
public T OriginalValue
{
get { return _originalValue; }
set
{
_originalValue = value;
RaisePropertyChanged(() => OriginalValue);
}
}
///
/// Determines if value is modified.
///
public bool IsDirty
{
get { return _isDirty; }
set
{
_isDirty = value;
RaisePropertyChanged(() => IsDirty);
}
}
///
/// A flag that determines whether this property can be marked as modified or not if value is changed.
///
public bool CanMarkDirty
{
get { return _canMarkDirty; }
set
{
_canMarkDirty = value;
RaisePropertyChanged(() => CanMarkDirty);
}
}
public EditableProperty(T value)
{
Value = value;
OriginalValue = value;
IsDirty = false;
CanMarkDirty = true;
}
///
/// Saves the value modification so that original value is consistent with value and modified flag is false.
///
public void SaveValue()
{
OriginalValue = Value;
IsDirty = false;
}
///
/// Discards the value modification so that original value is consistent with value and modified flag is false.
///
public void ResetValue()
{
Value = OriginalValue;
IsDirty = false;
}
}