Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF ComboBox SelectedItem Set to Null on TabControl Switch

Tags:

binding

wpf

xaml

I've got a simple problem in my WPF application which has me banging my head on the table. I have a TabControl, where every TabItem is a View generated for a ViewModel using a DataTemplate similar to this:

<DataTemplate DataType="{x:Type vm:FooViewModel}">
    <vw:FooView/>
</DataTemplate>

FooView contains a ComboBox:

<ComboBox ItemsSource="{Binding Path=BarList}" DisplayMemberPath="Name" SelectedItem="{Binding Path=SelectedBar}"/>

and FooViewModel contains a simple Property: public Bar SelectedBar { get; set; }. My problem is that when I set the value for my ComboBox, change to another tab, then change back, the ComboBox is empty again. If I set a breakpoint on the setter for my property, I see that the property is assigned to null when I switch to another tab.

From what I understand, when a tab is switched, it is removed from the VisualTree - but why is it setting my ViewModel's property to null? This is making it very difficult for me to hold persistent state, and checking value != null does not seem like the right solution. Can anyone shed some like on this situation?

Edit: The call stack at the setter breakpoint only shows [External Code] - no hints there.

like image 851
bsg Avatar asked Aug 10 '10 04:08

bsg


2 Answers

We just ran into the same problem. We found a blog entry describing the problem. It looks like it is a BUG in WPF and there is a workaround:

Specify the SelectedItem binding before the ItemsSource binding and the problem should be gone.

blog article

TLDR;

Change:

<ComboBox ItemsSource="{Binding Countries, Mode=OneWay}"
          SelectedItem="{Binding SelectedCountry}"        
          DisplayMemberPath="Name" >
</ComboBox>

To:

<ComboBox SelectedItem="{Binding SelectedCountry}"
          ItemsSource="{Binding Countries, Mode=OneWay}"
          DisplayMemberPath="Name" >
</ComboBox>
like image 196
Holger Adam Avatar answered Nov 03 '22 12:11

Holger Adam


My app is using avalondock & prims and had that exact problem. I has same thought with BSG, when we switched tab or document content in MVVM app, the controls as listview+box, combobox is removed from VisualTree. I bugged and saw most data of them was reset to null such as itemssource, selecteditem, .. but selectedboxitem was still hold current value.

A approach is in model, check its value is null then return like this:

 private Employee _selectedEmployee;
 public Employee SelectedEmployee
 {
     get { return _selectedEmployee; }
     set
     {
        if (_selectedEmployee == value || 
            IsAdding ||
            (value == null && Employees.Count > 0))
    {
        return;
    }

    _selectedEmployee = value;
    OnPropertyChanged(() => SelectedEmployee);
} 

But this approach can only solve quite good in first binding level. i mean, how we go if want to bind SelectedEmployee.Office to combobox, do same is not good if check in propertyChanged event of SelectedEmployee model.

Basically, we dont want its value is reset null, keep its pre-value. I found a new solution consistently. By using attached property, i created KeepSelection a-Pro, bool type, for Selector controls, thus supply all its inherited suck as listview, combobox...

public class SelectorBehavior
{

public static bool GetKeepSelection(DependencyObject obj)
{
    return (bool)obj.GetValue(KeepSelectionProperty);
}

public static void SetKeepSelection(DependencyObject obj, bool value)
{
    obj.SetValue(KeepSelectionProperty, value);
}

// Using a DependencyProperty as the backing store for KeepSelection.  This enables animation, styling, binding, etc...
public static readonly DependencyProperty KeepSelectionProperty =
    DependencyProperty.RegisterAttached("KeepSelection", typeof(bool), typeof(SelectorBehavior), 
    new UIPropertyMetadata(false,  new PropertyChangedCallback(onKeepSelectionChanged)));

static void onKeepSelectionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    var selector = d as Selector;
    var value = (bool)e.NewValue;
    if (value)
    {
        selector.SelectionChanged += selector_SelectionChanged;
    }
    else
    {
        selector.SelectionChanged -= selector_SelectionChanged;
    }
}

static void selector_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var selector = sender as Selector;

    if (e.RemovedItems.Count > 0)
    {
        var deselectedItem = e.RemovedItems[0];
        if (selector.SelectedItem == null)
        {
            selector.SelectedItem = deselectedItem;
            e.Handled = true;
        }
    }
}
}

Final, i use this approach simply in xaml:

<ComboBox lsControl:SelectorBehavior.KeepSelection="true"  
        ItemsSource="{Binding Offices}" 
        SelectedItem="{Binding SelectedEmployee.Office}" 
        SelectedValuePath="Id" 
        DisplayMemberPath="Name"></ComboBox>

But, selecteditem will never null if selector's itemssource has items. It may affect some special context.

Hope that helps. Happy conding! :D

longsam

like image 4
longsam Avatar answered Nov 03 '22 13:11

longsam