Search code examples
c#wpfxamlsystem.reactivemarkup-extensions

Markup extension in XAML for binding to ISubject<string>


If I have the following view model

class Foo : INotifyPropertyChanged {

    ISubject<string> Name { ... }

} 

and some imagined XAML code

<TextBox Text="{my:Subscribe Path=Name}/>

I wish the two way binding to behave that

  • Subject.onNext is called when the text box is updated in the UI
  • the text box is updated by subscribing to the Subject.Subscribe

As WPF only supports INPC directly my idea is to create a proxy INPC object in via a markup extension

class WPFSubjectProxy : INotifyPropertyChanged{

    string Value { ... }

}

The proxy would be wired up to the subject as so

subject.Subscribe(v=>proxy.Value=v);

proxy
    .WhenAny(p=>p.Value, p.Value)
    .Subscribe(v=>subject.OnNext(v))

Note WhenAny is a ReactiveUI helper for subscribing to INPC events.

But then I would need to generate a binding and return that via the markup extension.

I know what I want to do but can't figure out the Markup extension magic to put it all together.


Solution

  • It's hard to say without seeing specifically what you're struggling with, but perhaps this helps?

    EDIT

    The solution I (bradgonesurfing) came up with is below thanks to the pointer in the assigned correct answer.

        Nodes      

    and the implementing code. It has a dependency on ReactiveUI and a helper function in a private library for binding ISubject to a mutable property on an INPC supporting object

    using ReactiveUI.Subjects;
    using System;
    using System.Linq;
    using System.Reactive.Subjects;
    using System.Windows;
    using System.Windows.Data;
    using System.Windows.Markup;
    
    namespace ReactiveUI.Markup
    {
        [MarkupExtensionReturnType(typeof(BindingExpression))]
        public class SubscriptionExtension : MarkupExtension
        {
            [ConstructorArgument("path")]
            public PropertyPath Path { get; set; }
    
            public SubscriptionExtension() { }
    
            public SubscriptionExtension(PropertyPath path)
            {
                Path = path;
            }
    
            class Proxy : ReactiveObject
            {
                string _Value;
                public string Value
                {
                    get { return _Value; }
                    set { this.RaiseAndSetIfChanged(value); }
                }
            }
    
            public override object ProvideValue(IServiceProvider serviceProvider)
            {
                var pvt = serviceProvider as IProvideValueTarget;
                if (pvt == null)
                {
                    return null;
                }
    
                var frameworkElement = pvt.TargetObject as FrameworkElement;
                if (frameworkElement == null)
                {
                    return this;
                }
    
    
                object propValue = GetProperty(frameworkElement.DataContext, Path.Path);
    
                var subject = propValue as ISubject<string>;
    
                var proxy = new Proxy();
                Binding binding = new Binding() 
                {
                    Source = proxy,
                    Path = new System.Windows.PropertyPath("Value")
                };
    
                // Bind the subject to the property via a helper ( in private library )
                var subscription = subject.ToMutableProperty(proxy, x => x.Value);
    
                // Make sure we don't leak subscriptions
                frameworkElement.Unloaded += (e,v) => subscription.Dispose(); 
    
                return binding.ProvideValue(serviceProvider);
            }
    
            private static object GetProperty(object context, string propPath)
            {
                object propValue = propPath
                    .Split('.')
                    .Aggregate(context, (value, name)
                        => value.GetType() 
                            .GetProperty(name)
                            .GetValue(value, null));
                return propValue;
            }
    
        }
    }