jeudi 5 avril 2018

ComponentModel PropertyChanged binding not working on TransparentProxy

I have used RealProxy to create an 'Aspect' of sorts to fire the PropertyChanged event of INotifyPropertyChanged if the property is marked to notify. This, however, doesn't work. When debugging, I can see that the event is triggered and that the ComponentModel is in the InvocationList of the event, but my UI does not get updated. I have hooked into the event in my view model and it fires there as expected, but still no UI updates.

Here is some code to demonstrate the issue;

C#

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
using System.Windows;
using System.Windows.Controls;

namespace SOSample
{
    public abstract class NotifyBase : MarshalByRefObject, INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged([CallerMemberName] string propertyName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }


    [AttributeUsage(AttributeTargets.Property)]
    public class NotifyPropertyChangedAttribute : Attribute { }


    public class PropertyChangeAspect<T> : RealProxy where T : NotifyBase
    {
        private readonly T _decorated = default(T);

        private PropertyChangeAspect(T decorated) : base(typeof(T))
        {
            _decorated = decorated;
        }

        public static T Create(T decorated)
        {
            var aspect = new PropertyChangeAspect<T>(decorated);
            return (T)aspect.GetTransparentProxy();
        }

        public override IMessage Invoke(IMessage msg)
        {
            var methodCall = msg as IMethodCallMessage;
            if (methodCall == null) throw new ArgumentException(nameof(msg));

            try
            {
                var result = typeof(T).InvokeMember(methodCall.MethodName, BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance, null, _decorated, methodCall.Args);

                var methodInfo = methodCall.MethodBase as MethodInfo;
                if (methodInfo != null)
                {
                    var prop = FindProperty(methodInfo.Name);
                    var attr = prop?.GetCustomAttribute<NotifyPropertyChangedAttribute>(true);
                    if (attr != null) NotifyPropertyChanged(prop.Name);
                }
                return new ReturnMessage(result, methodCall.Args, methodCall.Args.Length, methodCall.LogicalCallContext, methodCall);
            }
            catch (Exception ex)
            {
                return new ReturnMessage(ex, methodCall);
            }
        }

        private PropertyInfo FindProperty(string name)
        {
            var props = _decorated.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
            var prop = props.FirstOrDefault(p => (p.SetMethod?.Name ?? string.Empty).Equals(name));
            return prop;
        }

        private void NotifyPropertyChanged(string propName)
        {
            var method = _decorated.GetType().GetMethod("NotifyPropertyChanged", BindingFlags.Instance | BindingFlags.Public, null, new Type[] { typeof(string) }, null);
            method.Invoke(_decorated, new object[] { propName });
        }
    }


    public class TestViewModel : NotifyBase
    {
        [NotifyPropertyChanged]
        public string Name { get; set; }

        // If I switch to this and don't use the proxy, it works fine
        //public string _name;
        //public string Name
        //{
        //    get => _name;
        //    set { _name = value; NotifyPropertyChanged(); }
        //}

        public TestViewModel()
        {
            PropertyChanged += PropChanged;
        }

        private void PropChanged(object sender, PropertyChangedEventArgs e)
        {
            Debug.WriteLine(e.PropertyName); // This writes to the output window, so I know the event is firing
        }
    }

    public partial class MainWindow : Window
    {
        public NotifyBase ViewModel { get; set; }
        public MainWindow()
        {
            InitializeComponent();
            //ViewModel = new TestViewModel(); // Using this works fine
            ViewModel = PropertyChangeAspect<TestViewModel>.Create(new TestViewModel()); // Using the proxy is problematic
            DataContext = this;
        }
    }

    public partial class TestView : UserControl
    {
        public TestView()
        {
            InitializeComponent();
        }
    }

}

Xaml

<Window x:Class="SOSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">

    <ContentPresenter Content="{Binding ViewModel}" />

</Window>


<UserControl x:Class="SOSample.TestView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="100" d:DesignWidth="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBox Grid.Row="0" Text="{Binding Name}" Margin="5" />
        <TextBox Grid.Row="1" Text="{Binding Name}" Margin="5" />
    </Grid>
</UserControl>

Can anyone tell me what is wrong, how I can fix it, or ask me to never post on SO again?





Aucun commentaire:

Enregistrer un commentaire