Reputation: 32182
If for example I have a view model class like
class ViewModel {
Data Data { get; set;}
}
and
class Data : IClonable {
public int Value0 {get; private set;}
public int Value1 {get; private set;}
Data SetValue0(int value){
var r = (Data) this.Clone();
r.Value0 = value;
return r;
}
Data SetValue1(int value){
var r = (Data) this.Clone();
r.Value1 = value;
return r;
}
}
in my XAML, with an instance of ViewModel as the DataContext, I would like to two way bind my text boxes like so
<TextBox Text="{Binding Data.Value0}"/>
<TextBox Text="{Binding Data.Value1}"/>
now obviously this doesn't work. What I need to explain to the binding is that to set the subproperties I have to call the method Set${propName}
and then replace the entire property from the root of the path.
Note the actual implementation I have of the immutable object pattern is much more complex than above but a solution for the above pattern I could extrapolate for my more complex setup. Is there anything I can provide to the binding to make it perform what I want?
For information my actual immutable object pattern allows things like
var newValue = oldValue.Set(p=>p.Prop1.Prop2.Prop3.Prop4, "xxx");
Upvotes: 1
Views: 1208
Reputation: 32182
I now have in my XAML code
<c:EditForLength Grid.Column="2" Value="{rx:ImmutableBinding Data.Peak}"/>
This becomes ImmutableBinding is a markup extension that returns a binding to a proxy object so you could imagine that the above is rewritten as
<c:EditForLength Grid.Column="2" Value="{Binding Value, ValidatesOnNotifyDataErrors=True}"/>
with the binding direct to the proxy object and the proxy object shadowing the real data and the validation errors. My code for the proxy and immutable binding object is
Note the code uses ReactiveUI calls and some of my own custom code but the general pattern of building a custom binding using a proxy to mediate validation errors should be clear. Also the code is probably leaking memory and I'll be checking that soon.
[MarkupExtensionReturnType(typeof(object))]
public class ImmutableBinding : MarkupExtension
{
[ConstructorArgument("path")]
public PropertyPath Path { get; set; }
public ImmutableBinding(PropertyPath path) { Path = path; }
/// <summary>
/// Returns a custom binding that inserts a proxy object between
/// the view model and the binding that maps immutable persistent
/// writes to the DTO.
/// </summary>
/// <param name="provider"></param>
/// <returns></returns>
override public object ProvideValue( IServiceProvider provider )
{
var pvt = provider as IProvideValueTarget;
if ( pvt == null )
{
return null;
}
var frameworkElement = pvt.TargetObject as FrameworkElement;
if ( frameworkElement == null )
{
return this;
}
if ( frameworkElement.DataContext == null )
{
return "";
}
var proxy = new Proxy();
var binding = new Binding()
{
Source = proxy,
Path = new PropertyPath("Value"),
Mode = BindingMode.TwoWay,
ValidatesOnDataErrors = true
};
var path = Path.Path.Split('.');
var head = path.First();
var tail = path.Skip(1)
.ToList();
var data = frameworkElement.DataContext as ValidatingReactiveObject;
if (data == null)
return null;
data.ErrorsChanged += (s, e) =>
{
if ( data.Errors.ContainsKey(Path.Path) )
{
proxy.Errors["Value"] = data.Errors[Path.Path];
}
else
{
proxy.Errors.Clear();
}
proxy.RaiseValueErrorChanged();
};
var subscription = data
.WhenAnyDynamic(path, change => change.Value )
.Subscribe(value => proxy.Value = value);
proxy
.WhenAnyValue(p => p.Value)
.Skip(1)
.DistinctUntilChanged()
.Subscribe
(value =>
{
var old = data.GetType()
.GetProperty(head)
.GetValue(data) as Immutable;
if (old == null) throw new NullReferenceException("old");
var @new = old.Set(tail, value);
data.GetType()
.GetProperty(head)
.SetValue(data, @new);
});
binding.ValidatesOnNotifyDataErrors = true;
return binding.ProvideValue(provider);
}
}
and the proxy object. Note that ValidatingReactiveObject implements INotifyDataErrorInfo
public class Proxy : ValidatingReactiveObject<Proxy>
{
object _Value;
public object Value
{
get { return _Value; }
set { this.ValidateRaiseAndSetIfChanged(ref _Value, value); }
}
public Proxy() { Value = 0.0; }
public void RaiseValueErrorChanged()
{
RaiseErrorChanged("Value");
RaiseErrorChanged("Error");
OnPropertyChanged("Error");
}
}
Upvotes: 1