Search code examples
.netwinformsdata-bindingmvvmnavigation-properties

Windows Forms data binding and property path: how to handle nullability?


I'm trying to implement a master/detail scenario with the MVVM pattern in a Windows Forms project (I'm mad, I know). Consider the following view models:

public class MasterViewModel
{
    public BindingList<DetailViewModel> Details { get; set; }

    public DetailViewModel SelectedDetail
    {
        get
        {
            //
        }
        set
        {
            // raises SelectedDetailChanged
        }
    }
}

public class DetailViewModel
{
    public string SubProperty
    {
        get
        {
            // ...
        }
        set
        {
            // ... raises SubPropertyChanged
        }
    }
}

I'm trying to bind DetailViewModel's SubProperty to a TextBox, using the following code (and the property path, supported by Windows Forms databinding):

            MasterViewModel masterViewModel;
            TextBox textBox;
            // ...

            Binding binding = new Binding("Text", masterViewModel, "SelectedDetail.SubProperty");
            binding.FormattingEnabled = true;
            binding.DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged;
            binding.ControlUpdateMode = ControlUpdateMode.OnPropertyChanged;

            textBox.DataBindings.Add(binding);

It works flawlessly (like WPF!!!)... until the SelectedDetail is null (for my logic, a SelectedDetail null means that, in the master view, nothing is selected). If SelectedDetail is null, I got an ArgumentNullException (parameter name: component).

Is there a way to handle the nullability of the "parent" property (inside a navigation path)?

Here is the exception stacktrace:

   in System.ComponentModel.ReflectPropertyDescriptor.AddValueChanged(Object component, EventHandler handler)
   in System.Windows.Forms.BindToObject.CheckBinding()
   in System.Windows.Forms.BindToObject.SetBindingManagerBase(BindingManagerBase lManager)
   in System.Windows.Forms.Binding.SetListManager(BindingManagerBase bindingManagerBase)
   in System.Windows.Forms.ListManagerBindingsCollection.AddCore(Binding dataBinding)
   in System.Windows.Forms.BindingsCollection.Add(Binding binding)
   in System.Windows.Forms.BindingContext.UpdateBinding(BindingContext newBindingContext, Binding binding)
   in System.Windows.Forms.Control.UpdateBindings()
   in System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnParentBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnParentBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnParentBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnParentBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnParentBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.set_BindingContextInternal(BindingContext value)
   in System.Windows.Forms.ContainerControl.set_BindingContext(BindingContext value)
   in System.Windows.Forms.ContainerControl.get_BindingContext()
   in System.Windows.Forms.Control.get_BindingContextInternal()
   in System.Windows.Forms.ContainerControl.get_BindingContext()
   in System.Windows.Forms.Control.get_BindingContextInternal()
   in System.Windows.Forms.ContainerControl.get_BindingContext()
   in System.Windows.Forms.Control.get_BindingContextInternal()
   in System.Windows.Forms.ContainerControl.get_BindingContext()
   in System.Windows.Forms.Control.get_BindingContextInternal()
   in System.Windows.Forms.Control.get_BindingContext()
   in System.Windows.Forms.Control.get_BindingContextInternal()
   in System.Windows.Forms.Control.get_BindingContext()
   in System.Windows.Forms.Control.UpdateBindings()
   in System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   in System.Windows.Forms.DataGridView.OnBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnParentBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnParentBindingContextChanged(EventArgs e)
   in System.Windows.Forms.Control.OnBindingContextChanged(EventArgs e)
   in System.Windows.Forms.ContainerControl.OnCreateControl()
   in System.Windows.Forms.UserControl.OnCreateControl()
   in System.Windows.Forms.Control.CreateControl(Boolean fIgnoreVisible)
   in System.Windows.Forms.Control.CreateControl(Boolean fIgnoreVisible)
   in System.Windows.Forms.Control.CreateControl(Boolean fIgnoreVisible)
   in System.Windows.Forms.Control.CreateControl(Boolean fIgnoreVisible)
   in System.Windows.Forms.Control.CreateControl()
   in System.Windows.Forms.Control.WmShowWindow(Message& m)
   in System.Windows.Forms.Control.WndProc(Message& m)
   in System.Windows.Forms.ScrollableControl.WndProc(Message& m)
   in System.Windows.Forms.ContainerControl.WndProc(Message& m)
   in System.Windows.Forms.Form.WmShowWindow(Message& m)
   in System.Windows.Forms.Form.WndProc(Message& m)
...
   in System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
   in System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   in System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

Solution

  • You need BindingSource to provide a layer of indirection.

    bindingSource1 = new BindingSource(components);
    bindingSource1.DataMember = "Details";
    bindingSource1.DataSource = typeof(MasterViewModel);
    
    Binding binding = new Binding("Text", bindingSource1, "SubProperty");
    binding.FormattingEnabled = true;
    binding.DataSourceUpdateMode = DataSourceUpdateMode.OnPropertyChanged;
    binding.ControlUpdateMode = ControlUpdateMode.OnPropertyChanged;
    
    textBox.DataBindings.Add(binding);
    

    A more detailed example: BindingSource and BindingNavigator in C# 2.0