autofac / Autofac.Extras.DynamicProxy

Interceptor and decorator support for Autofac IoC via Castle DynamicProxy
MIT License
106 stars 33 forks source link

PropertyChanged behavior broken if interface interception used in WPF app #53

Closed dtoth1 closed 1 year ago

dtoth1 commented 1 year ago

Describe the Bug

I'm trying to intercept viewmodels' interfaces by interface interceptors. Defined a simple logging interceptor, registered at type registration and opened new windows which's DataContext is binded to the before mentioned viewmodel. The viewmodel is resolved via viewmodel locator, which gets the viewmodel from the built container. If I define .EnableInterfaceInterceptors() at viewmodel registration, it's propertychanged behavior gets broken, the view is not aware of the binded property's changes. Without .EnableInterfaceInterceptors() the binding is working fine.

Steps to Reproduce

Register a viewmodel as an interface's implementation and enable interface interception on it. Created a test app as you can see below:

MainWindow, which registers the viewmodel and builds-, sets the container

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        var builder = new ContainerBuilder();

        builder.Register(c => new LoggingInterceptor());

        builder.RegisterType<BindedViewModel>().SingleInstance().As<IBindedViewModel>()
            .InstancePerLifetimeScope()
            .PropertiesAutowired();
            //.EnableInterfaceInterceptors();

        AfContainer.Container = builder.Build();
        ServiceLocator.SetLocatorProvider(() => new AutofacServiceLocator(AfContainer.Container));
    }

    private void Button_Click_3(object sender, RoutedEventArgs e)
    {
        var win = new BindedWindow();
        win.Show();
    }
}

Window which has binded property

<Window x:Class="AutofacTester.BindedWindow"
        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="BindedWindow" Height="250" Width="300"
        DataContext="{Binding Source={StaticResource Locator}, Path=BindedVM}">

    <Grid>
        <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Margin="5" Text="{Binding StatusText, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}"
                   FontSize="14"/>
    </Grid>
</Window>

Viewmodel's interface

namespace AutofacTester.Interfaces
{
    [Intercept(typeof(LoggingInterceptor))]
    public interface IBindedViewModel : INotifyPropertyChanged
    {
        string StatusText { get; }
    }
}

Viewodel

namespace AutofacTester.Implementations
{
    public sealed class BindedViewModel : IBindedViewModel
    {
        private readonly Timer _counterTimer;
        private int _counter;
        private string _statusText;

        public event PropertyChangedEventHandler? PropertyChanged;

        public string StatusText
        {
            get => _statusText;
            private set
            {
                _statusText = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusText)));
            }
        }

        public BindedViewModel()
        {
            StatusText = "Test";
            _counterTimer = new Timer(_ => SetStatusText(), null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));
        }

        private void SetStatusText()
        {
            _counter++;
            StatusText = $"Current counter: {_counter}";
            System.Diagnostics.Debug.WriteLine(">>> StatusText set to: " + StatusText);
        }
    }
}

Interceptor

public class LoggingInterceptor : IInterceptor
{
    void IInterceptor.Intercept(IInvocation invocation)
    {
        System.Diagnostics.Debug.WriteLine($">>>> Calling method {invocation.Method.Name} with parameters ({string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())})");
        invocation.Proceed();
        System.Diagnostics.Debug.WriteLine($">>>> Done: result was {invocation.ReturnValue}.");
        System.Diagnostics.Debug.WriteLine($">>>> Module: {invocation.TargetType.Module.Name}");
    }
}

ViewModel locator

public sealed class ViewModelLocator
{
    public IBindedViewModel BindedVM => ServiceLocator.Current.GetInstance<IBindedViewModel>();
}

A static class to hold the Container with the registered types

public static class AfContainer
{
    public static IContainer Container { get; set; }
}

Expected Behavior

Binding should work with .EnableInterfaceInterceptors() too.

Dependency Versions

TFM net6.0-windows WPF (UseWPF set to true in csproj)

Autofac: 7.0.0, tested with earlier versions as well Autofac.Extras.CommonServiceLocator: 6.0.1 Autofac.Extras.DynamicProxy: 6.0.1

tillig commented 1 year ago

I have to admit I'm not a WPF person and it'll take a bit before I can personally dive in here, but there are some steps you can take to help troubleshoot your own problem and maybe get an answer faster.

First, remove anything in the repro that doesn't contribute to showing the problem. Anything that isn't needed.

Why? Not only will it make it easier for us to look at, but sometimes removing stuff you think doesn't matter actually makes a difference.

~Next, try adding the PropertyChanged stuff to the interface. Again, not being a WPF guy, it may be that if the system doesn't see the event it won't get called.~ I see that the event is part of INotifyPropertyChanged. I still have to wonder if there's something missing during proxy generation, something I can't put my finger on.

Let us know how it goes.

tillig commented 1 year ago

I'd also recommend searching for "Castle DynamicProxy" along with "WPF" and "INotifyProertyChanged." Autofac is automatically wrapping the model, sure, but the challenge will be in making sure the proxy/interceptor is right. That part is Castle.DynamicProxy. Other people appear to have hit a similar issue while not using Autofac, so limiting your search to things using Autofac may not find you the answer.

dtoth1 commented 1 year ago

Dear @tillig, thanks for the comments, I removed the empty constructors and custom attributes, updated the code with AfContainer class as well. The Debug.WriteLines are present in code to mimic logging behavior without using a real logger implementation here in test app. I will search a little bit about Castle's dynamic proxy as well.

tillig commented 1 year ago

I don't think we can do anything here. If you find out that we can, we'd be happy to check out a pull request. Thanks!