BindingExpression path error when dynamically loading a UserControl

Jul 30, 2014 at 6:56 PM
I created a simple solution which demonstrates the problem I am having. I hope isolating it as such will help solve this problem. I am using UpdateControls 2.2.5.2 with .NET 4.0.

The projects in my solution are as follows:

My.Control is a simple user control which sets the DataContext to a class in My.ViewModel. The class in My.ViewModel uses an Independent<string>member to implement a StatusText property. This StatusText property is used by the simple User Control.
<UserControl x:Class="My.Control.UserControl1"
             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="300" d:DesignWidth="300">
    <DockPanel>
        <Label DockPanel.Dock="Top" Content="My Control" HorizontalAlignment="Center"/>
        <Label DockPanel.Dock="Bottom" Content="{Binding Path=StatusText}" HorizontalAlignment="Center"/>
    </DockPanel>
</UserControl>
Here is UserControl1's constructor:
        public UserControl1()
        {
            InitializeComponent();

            this.DataContext = UpdateControls.XAML.ForView.Wrap(new UserRepositoryViewModel());
        }
And here is the simply View Model class from above:
    public class UserRepositoryViewModel
    {
        private Independent<string> _statusText = new Independent<string>();

        public UserRepositoryViewModel()
        {
            StatusText = "Initializing...";

            Task.Factory.StartNew(() => SimulateSomeBackgroundWork() );
        }

        private void SimulateSomeBackgroundWork()
        {
            System.Threading.Thread.Sleep(500);
            Application.Current.Dispatcher.Invoke(new Action(() => StatusText = "Working..."));
            System.Threading.Thread.Sleep(500);
            Application.Current.Dispatcher.Invoke(new Action(() => StatusText = "Finishing..."));
            System.Threading.Thread.Sleep(500);
            Application.Current.Dispatcher.Invoke(new Action(() => StatusText = "Done."));
        }

        public string StatusText
        {
            get { return _statusText.Value; }
            set { if (value != _statusText.Value) _statusText.Value = value; }
        }
    }
I created 2 Wpf applications: RegularWpfApplication is a window which contains the User Control above and shows that the binding works as expected. So far so good.

StandaloneDynamicLoadApp is the 2nd WPF application, which as the name implies does not directly reference the control or view model assemblies, rather it loads those at runtime using Assembly.LoadFrom and instantiates the UserControl with System.Activator.CreateInstance. The dynamic loading of the user control works just fine and we have something in production today that does this. You can see the control in the 2nd application but the StatusText never changes and the output console reads:

System.Windows.Data Error: 40 : BindingExpression path error: 'StatusText' property not found on 'object' ''ObjectInstance1' (HashCode=44489159)'. BindingExpression:Path=StatusText; DataItem='ObjectInstance1' (HashCode=44489159); target element is 'Label' (Name=''); target property is 'Content' (type 'Object')

Here is the dynamic loading MainWindow code:
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        DynamicallyLoadControl();
    }

    private void DynamicallyLoadControl()
    {
        const string AssemblyFileSuffix = ".dll";
        string targetAssemblyName = "My.Control" + AssemblyFileSuffix;
        UserControl targetControl = null;
        string controlDir = @"..\..\..\My.Control\bin\Debug\";
        DirectoryInfo dirInfo = new DirectoryInfo(controlDir);
        var dependencies = dirInfo.GetFiles("*" + AssemblyFileSuffix, SearchOption.AllDirectories);

        Assembly targetAssembly = null;
        foreach (var dependency in dependencies)
        {
            string dependencyName = dependency.Name.Replace(AssemblyFileSuffix, string.Empty);
            var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name.Equals(dependencyName)).FirstOrDefault();
            if (null == loadedAssembly)
            {
                var assembly = Assembly.LoadFrom(dependency.FullName);
                loadedAssembly = AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name.Equals(assembly.GetName().Name)).FirstOrDefault();

                if (null == loadedAssembly)
                {
                    AppDomain.CurrentDomain.Load(assembly.GetName());
                }
                if (targetAssemblyName == dependency.Name)
                    targetAssembly = assembly;
            }
        }
        if (null == targetAssembly)
            throw new ArgumentOutOfRangeException("Could not find target assembly to load " + targetAssemblyName);
        foreach (Type aType in targetAssembly.GetTypes())
        {
            // Check to see if aType implements IAbfControl
            if (typeof(UserControl).IsAssignableFrom(aType))
            {
                targetControl = LoadToolControl(targetAssembly, aType);
            }
        }
    }

    private UserControl LoadToolControl(Assembly targetAssembly, Type targetType)
    {
        object targetControl = Activator.CreateInstance(targetType);

        surfaceContent.Children.Add((UIElement)targetControl);

        UserControl aUserControl = targetControl as UserControl;

        return aUserControl;
    }
}
Coordinator
Jul 31, 2014 at 4:14 PM
The best guess I have is that the TypeDescriptionProvider used internally doesn't take effect when loaded in this way. I'll have to investigate to see why.

In the meantime, you can use this workaround. Instead of ForView.Wrap, you can use the ViewModelBase class:
public class UserRepositoryViewModel : ViewModelBase
{
    // ...

    public string StatusText
    {
        get { return Get(() => _statusText.Value); }
        set { if (value != _statusText.Value) _statusText.Value = value; }
    }
}
Hope this helps.
Coordinator
Jul 31, 2014 at 5:28 PM
I found another way to inject the type descriptor. I was using an attribute. Now I'm attaching it at runtime to the instance. This fixes your repro.

I pushed 2.2.5.3 to NuGet. I'll also apply the fix to the trunk.
Marked as answer by hmelende5121 on 7/31/2014 at 1:32 PM
Jul 31, 2014 at 8:32 PM
Excellent! I see it works now. Thank you!