Eric Eggers
Eric Eggers

Reputation: 84

Can't data-bind to ComboBox in WPF control for WinForms app; designer works fine

I have a WPF user control that sits in an ElementHost so it can be used in a WinForms app. On this WPF user control is a ComboBox, to which I'm trying to data-bind an ObservableCollection of a rather simple custom class. This ObservableCollection is a public property of my "view model" class (the MVVM methodology is not perfect) which I bind to the user control (view), and I have a mock subclass to which I bind at design time.

The ComboBox renders exactly as expected in the XAML designer, but at runtime it's completely empty, with the drop-down area about 3 rows high, no matter how many items I add to it (the number of items never changes during the app's lifetime). There are other controls like TextBlocks and a homemade NumericUpDown that bind to other properties and operate just fine at runtime, it's the ComboBox that won't cooperate.

One other thing, should it matter--the view model instance is deserialized from a file. I have yet to include the properties I've added to it in the (de)serialization, but this matters because the instance will be created, then I have to call a function to finish the initialization since the constructor is bypassed, and this finish-up includes initializing the databound collection of items. I don't know if this is related to the problem, but I thought I'd mention it in case it is.

The item class. DescriptionText may change if the user selects a new language:

namespace Company._DataBinding {
  
  public class FlavorOption {
    public event PropertyChangedEventHandler PropertyChanged;
    private string _descriptionText;

  public EnumFlavor Flavor { get; set; }

  public string DescriptionText {
    get { return _descriptionText; }
    set {
      if (_descriptionText != value) {
        _descriptionText = value;
        try {
          PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("DescriptionText"));
        } catch (Exception anException) {
          throw anException;
        }
      }
    }
  }
}

Abridged XAML of the user control. The code-behind calls InitializeComponent inside the constructor and implements IProcessOrder. Otherwise, it doesn't touch anything, including JuiceChoicesText, JuiceFlavorOptions, ChosenJuice, or the controls here:

<UserControl x:Class="Company._Specifics.JuiceOrderView"
         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"
         xmlns:local="clr-namespace:Company"
         xmlns:bound="clr-namespace:Company._DataBinding"
         mc:Ignorable="d" 
         d:DesignHeight="405" d:DesignWidth="612">
<Grid Background="White" d:DataContext="{d:DesignInstance Type=bound:MockProtoVM, IsDesignTimeCreatable=True}">
    <StackPanel>
        <TextBlock Text="{Binding JuiceChoicesText}" Grid.Column="2" />
        <ComboBox Margin="5" ItemsSource="{Binding JuiceFlavorOptions}"
     SelectedValuePath="Flavor" DisplayMemberPath="DescriptionText"
     SelectedValue="{Binding ChosenJuice, Mode=TwoWay}" />
    </StackPanel>
</Grid>

Relevant "view model" code:

namespace Maf._Specifics {
  public class ProtoVM : INotifyPropertyChanged {
    public event PropertyChangedEventHandler PropertyChanged;
    private string _juiceChoicesText;
    private EnumFlavor _chosenJuice;
    
    public ObservableCollection<FlavorOption> JuiceFlavorOptions { get; set; }

    public string JuiceChoicesText {
      get { return _juiceChoicesText; }
      set {
        if (_juiceChoicesText != value) {
          _juiceChoicesText = value;
          NotifyListeners();
        }
      }
    }

    public EnumFlavor ChosenJuice {
      get { return _chosenJuice; }
      set {
        if (_chosenJuice != value) {
          _chosenJuice = value;
          NotifyListeners();
        }
      }
    }

    public ProtoVM() {
      FinishInitialization();
    }

    public void FinishInitialization() {
      JuiceChoicesText = "Choose your juice flavor:";

      if (JuiceFlavorOptions == null) {
        JuiceFlavorOptions = new ObservableCollection<FlavorOption> {
         new FlavorOption { Flavor = EnumFlavor.CHERRY, DescriptionText = "Cherry" },
         new FlavorOption { Flavor = EnumFlavor.ORANGE, DescriptionText = "Orange" },
         new FlavorOption { Flavor = EnumFlavor.GRAPE, DescriptionText = "Grape" }
      };

      ChosenJuice = EnumFlavor.CHERRY;
    }

    protected void NotifyListeners([CallerMemberName] string propertyName = "") {
      try {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
      } catch (Exception anException) {
        throw anException;
      }
    }
  }
}

Setting up the user control in its parent control(s). No options are added to or removed from the ComboBox('s DataContext) after the data-bind:

var juiceOrderHost = new WPFWrapperUC();
var orderJuiceTab = new TabPage(tabName);
_juiceOrderView = new JuiceOrderView();
juiceOrderHost.SetChild(_juiceOrderView);
      orderJuiceTab.Controls.Add((UserControl)juiceOrderHost);
tbcThingsToOrder.TabPages.Add(orderJuiceTab);
...
_juiceOrderKindaVM.FinishInitialization();  // _juiceOrderKindaVM is a ProtoVM
_juiceOrderView.DataContext = _juiceOrderKindaVM;

The WinForms UserControl, which has on it just a System.Windows.Forms.Integration.ElementHost:

namespace Company {
  public partial class WPFWrapperUC : UserControl, IProcessOrder {
    public WPFWrapperUC(UIElement hosting = null) {
      InitializeComponent();

      if (hosting != null)
        SetChild(hosting);
    }

    public void SetChild(UIElement hosting) {
      elhHostForWPFControl.Child = hosting;
    }

    // ...various methods that forward calls to
    //  IProcessOrder methods to elhHostForWPFControl.Child,
    //  should it be a IProcessOrder, which JuiceOrderView is. 
  }
}

The mock:

namespace Company._DataBinding {
  public class MockProtoVM : ProtoVM {
    public MockProtoVM() : base() {
      JuiceChoicesText = "User sees different text at runtime, like 'Choose your juice flavor:'";
      ChosenJuice = EnumFlavor.ORANGE;
    }
  }
}

So, what am I missing? Thanks...

Upvotes: 0

Views: 41

Answers (1)

Eric Eggers
Eric Eggers

Reputation: 84

After more research/spitballing, it turns out that before it got to the DataContext assignment shown above, DataContext was previously being assigned to the ProtoVM instance before FinishInitialization had been called, and so JuiceFlavorOptions was null. It then called FinishInitialization again on this same instance, but apparently setting JuiceFlavorOptions then wasn't enough? It then assigned the instance to the DataContext, and that fixed the TextBlock but not the ComboBox. At least, this is my best guess at what happened.

Also, my translation/swap-out of FlavorOption.DescriptionText needed a little more than just iterating through JuiceFlavorOptions and updating the DescriptionText value of each FlavorOption. That would show the right text when selecting a new option, but the dropdown would still have all the old options' text. You must clear out JuiceFlavorOptions and repopulate it with the new FlavorOption instances with their new DescriptionText values. And to keep the ComboBox from looking unselected, you must then invoke the property changed delegate on ChosenJuice to fully update the binding:

  JuiceFlavorOptions.Clear();
  // French
  JuiceFlavorOptions.Add(new FlavorOption { Flavor = EnumFlavor.CHERRY, DescriptionText = "Cerise" });
  JuiceFlavorOptions.Add(new FlavorOption { Flavor = EnumFlavor.ORANGE, DescriptionText = "Orange" });
  JuiceFlavorOptions.Add(new FlavorOption { Flavor = EnumFlavor.GRAPE, DescriptionText = "Raisin" });
  NotifyListeners("ChosenJuice");

Hope this helps someone who manages to get into my weird situation.

Upvotes: 0

Related Questions