views:

503

answers:

3

I have the following XAML markup:

<TextBox x:Name="MyTextBox" Text="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Name="MyComboBox" ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"
    SelectedValue="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber}"
    SelectedValuePath="ProductNumber" />

My View's DataContext is bound to a viewmodel containing a public property called SelectedCustomer. Customer objects contain a FavouriteProduct property of type Product and Product objects contain public properties ProductNumber and ProductName.

The behaviour I'm looking for is to have the SelectedItem of the ComboBox update the Text in the TextBox and vice versa. ComboBox to TextBox works just fine. Selecting any product in the ComboBox updates the TextBox with the product number of that product. However when I try to go the other way I get som strange behaviour. It only works for the items that come before the selected item. I will try to explain:

Consider the following list of products ([Product Number], [Product Name]):

  1. Fanta
  2. Pepsi
  3. Coca Cola
  4. Sprite
  5. Water

Now lets say that the SelectedCustomer's favourite product is Coca Cola (must be a developer). So when the window opens the TextBox reads 3 and the ComboBox reads Coca Cola. Lovely. Now lets change the product number in the TextBox to 2. The ComboBox updates it's value to Pepsi. Now try to change the product number in the TextBox to anything higher then the number for Coca Cola (3). Not so lovely. Selecting either 4 (Sprite) or 5 (Water) makes the ComboBox revert back to Coca Cola. So the behaviour seems to be that anything below the item that you open the window width from the list in the ItemSource does not work. Set it to 1 (Fanta) and none of the others work. Set it to 5 (Water) and they all work. Could this have to do with some initialisation for the ComboBox? Potential bug? Curious if anyone else have seen this behaviour.

UPDATE:

After reading Mike Brown's response I have created properties for SelectedProduct and SelectedProductNumber. The problem I am having with this is that as soon as you select something from the ComboBox you end up in an endless loop where the properties keep updatign each other. Have I implemented the OnPropertyChanged handler incorrectly or is there something I am missing? Here is a snippet of code from my ViewModel:

private int _SelectedProductNumber = -1;
     public int SelectedProductNumber
     {
      get
      {
       if (_SelectedProductNumber == -1 && SelectedCustomer.Product != null)
        _SelectedProductNumber = SelectedCustomer.Product.ProductNumber;
       return _SelectedProductNumber;
      }
      set
      {
       _SelectedProductNumber = value;
       OnPropertyChanged("SelectedProductNumber");
       _SelectedProduct = ProductList.FirstOrDefault(s => s.ProductNumber == value);
      }
     }

     private Product _SelectedProduct;
     public Product SelectedProduct
     {
      get
      {
       if (_SelectedProduct == null)
        _SelectedProduct = SelectedCustomer.Product;
       return _SelectedProduct;
      }
      set
      {
       _SelectedProduct = value;
       OnPropertyChanged("SelectedProduct");
       _SelectedProductNumber = value.ProductNumber;
      }
     }

     public event PropertyChangedEventHandler PropertyChanged;

     protected virtual void OnPropertyChanged(string property)
     {
      if (PropertyChanged != null)
       PropertyChanged(this, new PropertyChangedEventArgs(property));
     }

UPDATE 2

I have changed the implementation slightly now by updating the SelectedCustomer.FavouriteProduct from both properties and then using that when reading their values. This now works but I'm not sure it's the 'correct way'.

private int _SelectedProductNumber = 0;
public int SelectedProductNumber
{
    get
    {
     if (SelectedCustomer.Product != null)
      _SelectedProductNumber = SelectedCustomer.Product.ProductNumber;
     return _SelectedProductNumber;
    }
    set
    {
     _SelectedProductNumber = value;
     SelectedCustomer.FavouriteProduct = ProductList.FirstOrDefault(s => s.ProductNumber == value);
     OnPropertyChanged("SelectedProductNumber");
     OnPropertyChanged("SelectedProduct");
    }
}

private Product _SelectedProduct;
public Product SelectedProduct
{
    get
    {
     if (SelectedCustomer.Product != null)
      _SelectedProduct = SelectedCustomer.Product;
     return _SelectedProduct;
    }
    set
    {
     _SelectedProduct = value;
     SelectedCustomer.FavouriteProduct = value;
     OnPropertyChanged("SelectedProduct");
     OnPropertyChanged("SelectedProductNumber");
    }
}
A: 

Try: SelectedItem="{Binding Path=YourPath, Mode=TwoWay"} instead of setting SelectedValue and SelectedValuePath.

Might work with SelectedValue too, don't forget the Mode=TwoWay, since this isn't the default.

A good approuch would to use the master detail pattern - bind the master (the items view, e.g. combobox) to the data source collection and the detail view (e.g. text box) to the selected item in the source collection, using a binding converter to read/write the appropriate property.

Here is an example: http://blogs.microsoft.co.il/blogs/tomershamam/archive/2008/03/28/63397.aspx

Notice the master binding is of the form {Binding} or {Binding SourceCollection} and the details binding is of the form {Binding } or {Binding SourceCollection}.

To get this working you need to wrap you collection with an object that keeps the selected item. WPF has one of these built-in: ObjectDataProvider.

Example: http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/068977c9-95a8-4b4a-9d38-b0cc36d06446

Danny Varod
I don't see how I can use SelectedItem to set the selected Product based on ProductNumber
Christian Hagelid
Edited answer to address the issue.
Danny Varod
+1  A: 

Your aim is not too clear so I have written the folloiwng so support either options I can see.

To keep two elements bound to one item in sync you can set the IsSynchronizedWithCurrentItem="True" on your combobox as shown below:

<TextBox x:Name="MyTextBox" Text="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox x:Name="MyComboBox" ItemsSource="{Binding Products}" DisplayMemberPath="ProductName"
    SelectedValue="{Binding Path=SelectedCustomer.FavouriteProduct.ProductNumber}"
    IsSynchronizedWithCurrentItem="True"
    SelectedValuePath="ProductNumber" />

This will mean everything in the current window bound to the same background object will keep in sync and not give the odd behaviours you are seeing.

This quote form this longer MSDN article describes the effect:

The IsSynchronizedWithCurrentItem attribute is important in that, when the selection changes, that is what changes the "current item" as far as the window is concerned. This tells the WPF engine that this object is going to be used to change the current item. Without this attribute, the current item in the DataContext won't change, and therefore your text boxes will assume that it is still on the first item in the list.

Then setting the Mode=TwoWay as suggested by the other answer will only ensure that both when you update the textbox the underlying object will be updated and when you update the object the textbox is updated.

This makes the textbox edit the selected items text and not select the item in the combolist with the matching text (which is the alternative think you are may be trying to achieve?)

To achieve the synchronised selection effect it may be worth setting IsEditable="True" on the combobox to allow users to type items in and dropping the text box. Alternatively if you need two boxes replace the textbox with a second combobox with IsSynchronizedWithCurrentItem="True" and IsEditable="True" then a styled to make it like a text box.

John
This looked very promising but unfortunately I'm still getting the same behaviour. The TextBox gets updated when I select something from the ComboBox but the other way is not working properly. I think I might be going about it all wrong.
Christian Hagelid
Are you looking for the text in the textbox to changes the selection or to change/edit the text of the selected item? I have updated the answer with both solutions.
John
I am aiming to give the user two options to select a product. Either by selecting the product from the ComboBox or by typing in the product number. I can make the ComboBox editable but that will not let me type in product numbers. I have not looked into using a separate ComboBox with the IsSynchronizedWithCurrentItem option yet.
Christian Hagelid
+1  A: 

What you want to do is expose separate properties on your ViewModel for the currently selected product and currently selected product number. When the selected product is changed, update the product number and vice versa. So your viewmodel should look something like this

public class MyViewModel:INotifyPropertyChanged
{
    private Product _SelectedProduct;
    public Product SelectedProduct
    {
        get { return _SelectedProduct; }
        set
        {
            _SelectedProduct = value;
            PropertyChanged(this, new PropertyChangedEventArgs("SelectedProduct"));
            _SelectedProductID = _SelectedProduct.ID;
            PropertyChanged(this, new PropertyChangedEventArgs("SelectedProductID"));
        }
    }
    private int _SelectedProductID;
    public int SelectedProductID
    {
        get { return _SelectedProductID; }
        set
        {
            _SelectedProductID = value;
            PropertyChanged(this, new PropertyChangedEventArgs("SelectedProductID")); 
            _SelectedProduct = _AvailableProducts.FirstOrDefault(p => p.ID == value); 
            PropertyChanged(this,new PropertyChangedEventArgs("SelectedProduct"));
        }
    }  
    private IEnumerable<Product> _AvailableProducts = GetAvailableProducts();

    private static IEnumerable<Product> GetAvailableProducts()
    {
        return new List<Product>
                   {
                       new Product{ID=1, ProductName = "Coke"},
                       new Product{ID = 2, ProductName="Sprite"},
                       new Product{ID = 3, ProductName = "Vault"},
                       new Product{ID=4, ProductName = "Barq's"}
                   };
    }

    public IEnumerable<Product> AvailableProducts
    {
        get { return _AvailableProducts; }
    }  
    private Customer _SelectedCustomer; 
    public Customer SelectedCustomer
    {
        get { return _SelectedCustomer; } 
        set
        {
            _SelectedCustomer = value; 
            PropertyChanged(this, new PropertyChangedEventArgs("SelectedCustomer")); 
            SelectedProduct = value.FavoriteProduct;
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
}

So now your XAML binds to the appropriate properties and the viewModel is responsible for syncrhronization

<TextBox 
  x:Name="MyTextBox" 
  Text="{Binding Path=SelectedProductID, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox 
  x:Name="MyComboBox" 
  ItemsSource="{Binding AvailableProducts}" 
  DisplayMemberPath="ProductName" 
  SelectedItem="{Binding SelectedProduct}" />

Don't forget to implement the rest of INotifyPropertyChanged and the GetAvailableProducts function. Also there may be some errors. I hand typed this here instead of using VS but you should get the general idea.

Mike Brown
I thought I might have to do something like this. I have added the respective properties but I am having a problem between the ProductId and the Product properties. As soon as you change either of them you get into an endless loop and finally get a StackOverflow exception ;) It might be because of how I am implementing the handler though (I'm fairly new to WPF). I'll update the question with the code.
Christian Hagelid
Sorry that was boneheaded of me...rather than updating the opposite property just update the backing field and call property changed...I'll update the code with the correct implementation.
Mike Brown
Okay done...that should work for you now...they should add a special badge for answers that result in stack overflows ;)
Mike Brown
hmm...I'm still getting the same error with this code. As soon as I update the SelectedProductNumber from the SelectedProduct property (or vice versa) I get into en endless loop. I have implemented the solution slightly different now by updating the SelectedCustomer.FavouriteProduct field from both properties (see Update 2)
Christian Hagelid