thecoop
thecoop

Reputation: 46098

Binding two-way to a readonly property

I've got a read-only property on a model object, but want to bind two-way to it. When the binding source should get updated, I want the update to instead be 'redirected' to a separate method that plugs into our controller infrastructure to perform the update and generate a new model with the change property. Simply using a Binding on a read-only property and adding a handler to the Binding.TargetUpdated event doesn't work (throws an InvalidOperationException, specifying the property should be read-only).

There's a simple solution to this (create a copy of the model, with read-write properties that do the redirect for me) but I don't really want to have to copy all my model objects. Is there a way of doing this programatically?

Upvotes: 0

Views: 727

Answers (3)

thecoop
thecoop

Reputation: 46098

I found a solution using ExpandoObject - given a read-only model object, this generates a read-write model at runtime that calls the controller method whenever any property changes:

public static dynamic GetAdapterFor(IController controller, object modelObj)
{
    if (modelObj == null)
        return null;

    ExpandoObject obj = new ExpandoObject();

    // add all the properties in the model
    foreach (var prop in modelObj.GetType().GetProperties())
    {
        ((IDictionary<string, object>)obj).Add(prop.Name, prop.GetValue(modelObj, null));
    }

    // add the handler to update the controller when a property changes
    ((INotifyPropertyChanged)obj).PropertyChanged += (s, e) => UpdateController(controller, e.PropertyName, ((IDictionary<string, object>)s)[e.PropertyName]);

    return obj;
}

private static void UpdateController(IController controller, string propertyName, object propertyValue)
{
    controller.SetPropertyValue(propertyName, propertyValue);
}

Upvotes: 0

Lubo
Lubo

Reputation: 754

This solution is based on custom markup extension which setups a two way binding for some dependency property. The binding uses some kind of wrapper with writable property as a source. The wrapper calls the infrastructure code to update and generate new model after property changed.

Below example with hard coded scenario, but I think the idea is quite clear.

namespace MyApp
{

public class MyModel
{
    //readonly property
    public string Name { get; private set; }

    public MyModel(string name)
    {
        Name = name;
    }
}

public class MyViewModel
{
    public MyModel Model { get; set; }

    public MyViewModel()
    {
        Model = new MyModel("default");
    }
}

public class Wrapper
{
    public MyViewModel ViewModel { get; set; }

    //writable property to enable two-way binding
    public object Value
    {
        get
        {
            return ViewModel.Model.Name;
        }
        set
        {
            //call your infrastructure method to 
            //update and generate new model
            ViewModel.Model = new MyModel((string)value);
        }
    }
}

[MarkupExtensionReturnType(typeof(Object))]
public class CustomBinding : MarkupExtension
{
    //you can add any properties here for your infrastructure method call
    //public string PropertyName { get; set; }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        var provideValueTarget = serviceProvider.GetService(typeof(IProvideValueTarget)) as IProvideValueTarget;
        var binding = new Binding()
        {
            //get whatever you need from target element to setup the binding and wrapper
            Source = new Wrapper()
            {
                ViewModel = (provideValueTarget.TargetObject as FrameworkElement).DataContext as MyViewModel
            },
            Path = new PropertyPath("Value")
        };
        var result = binding.ProvideValue(serviceProvider);
        return result;
    }
}

}
XAML
<StackPanel>
    <StackPanel.DataContext>
    <MyApp:MyViewModel />
    <StackPanel.DataContext>
    <TextBox Text="{MyApp:CustomBinding}" />
<StackPanel />

Upvotes: 1

Drew Noakes
Drew Noakes

Reputation: 310897

I don't think you'll get away without binding to a new intermediate object. Some might call it a view model. I was wondering whether a custom IValueConverter might be able to intercept the write operation as you need in ConvertBack, but I'm reasonably sure that the binding system won't even try to call the converter if the source property isn't writable.

Upvotes: 0

Related Questions