I am doing something wrong .. you know how it is.
I have tried playing around with ItemsSource , DataContext , DisplayMemberPath and SelectedValuePath and I either get a blank list of a list of the ToString method being called in the Person object;
WHAT WOULD REALLY HELP is for someone to publish an answer that works for this example.
I have simplified the problem as I am having difficulty in general with databinding generics.
I have created a simple Generic List of Person and want to bind it to a combo. (also want to try use a ListView too).
I either get a list of blanks or a list of 'xxxx.Person' where xxxx = namespace
<Window x:Class="BindingGenerics.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="300">
<Grid>
<ComboBox Name="ComboBox1"
ItemsSource="{Binding}"
Height="50"
DisplayMemberPath="Name"
SelectedValuePath="ID"
FontSize="14"
VerticalAlignment="Top">
</ComboBox>
</Grid>
</Window>
using System.Windows;
using System.ComponentModel;
namespace BindingGenerics
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Person p = new Person();
// I have tried List and BindingList
//List<Person> list = new List<Person>();
BindingList<Person> list = new BindingList<Person>();
p.Name = "aaaa";
p.ID = "1111";
list.Add(p);
p = new Person();
p.Name = "bbbb";
p.ID = "2222";
list.Add(p);
p = new Person();
p.Name = "cccc";
p.ID = "3333";
list.Add(p);
p = new Person();
p.Name = "dddd";
p.ID = "4444";
list.Add(p);
ComboBox1.DataContext = list;
}
}
public struct Person
{
public string Name;
public string ID;
}
}
In your code sample, Person.Name is a field rather than a property. WPF data binding considers only properties, not fields, so you need to change Person.Name to be a property.
Change your Person declaration to:
public class Person
{
public string Name { get; set; }
public string ID { get; set; }
}
(For production code, you'll probably want to use an ObservableCollection<Person> rather than a List<Person> and either make Person immutable or make it implement INotifyPropertyChanged -- but those aren't the sources of your immediate problem.)
In the code shown you're setting ItemsSource twice, the first time in XAML (called by InitializeComponent) to the DataContext of ComboBox1, which can't be determined from what you've posted but it's probably not what you want. After that you're resetting it from code to your list object (here with typos). In this code you're also adding the same instance of Person 4 times and just changing its Name and ID over and over. I suspect a combination of these issues and the fact that you're using a List instead of ObservableCollection are causing the issues in your application.
It would help narrow it down if you could post some actual code you're seeing problems with as what you've put here isn't even compilable.
Well... I'm assuming your actual code has corrected syntax, as the code you pasted in won't compile.
I put this code into a new WPF app and, after new-ing each Person object, my combobox populated fine. You might want to move your population code into a Loaded event, which will ensure the form is properly constructed. Here's the corrected xaml and codebehind (with a few syntax shortcuts):
xaml:
<Grid>
<ComboBox Name="ComboBox1" Height="70"
DisplayMemberPath="Name"
SelectedValuePath="ID" />
</Grid>
codebehind:
public Window1()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Window1_Loaded);
}
void Window1_Loaded(object sender, RoutedEventArgs e)
{
var list = new List<Person>();
Person p = new Person(){Name = "aaaa",ID = "1111"};
list.Add(p);
p = new Person(){Name = "bbbb", ID="2222"};
list.Add(p);
p = new Person(){Name = "cccc", ID="3333"};
list.Add(p);
p = new Person(){Name = "dddd", ID="4444"};
list.Add(p);
ComboBox1.ItemsSource = list;
}
Related
This has me stumped - hopefully someone can point out an obvious error. I have a user control that I am adding to a grid in the MainView of my program. Main view is bound to MainViewModel and the usercontrol is bound to CardioVM.
I have used a test label to check that the routing of the user control is correct and all work ok. I have a class named Cardio which has a property of
List<string> exercises
I am trying to pass the strings in
Cardio.List<string> exercises
to a
List<string> CardioList
in my CardioVM. When debugging
List<string> CardioList
is getting populated with items from
Cardio.List<string> exercises
but my ComboBox is not displaying the items on screen. Here is xaml for my UserControl and :
<UserControl x:Class="CalendarTest.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"
DataContext="{Binding CardioVM, Source={StaticResource Locator}}">
<Grid>
<ComboBox ItemsSource="{Binding CardioList, Mode=OneWay}" SelectedItem="{Binding SelectedCardio, Mode=TwoWay}" Height="50"></ComboBox>
</Grid>
and here is the code for my CardioVM:
public class CardioVM : ViewModelBase
{
public Cardio cardioItem { get; set; }
public CardioVM()
{
TestLabel = "Tester";
}
//Test Label for binding testing
private string testLabel;
public string TestLabel
{
get { return testLabel; }
set
{
testLabel = value;
RaisePropertyChanged("TestLabel");
}
}
public CardioVM(string Date, string File)
{
cardioItem = new Cardio(File, Date);
CardioList = new List<string>(cardioItem.exercises);
}
private List<string> cardioList;
public List<string> CardioList
{
get { return cardioList; }
set
{
cardioList = value;
RaisePropertyChanged("CardioList");
}
}
private string _selectedCardio;
public string SelectedCardio
{
get { return _selectedCardio; }
set
{
_selectedCardio = value;
RaisePropertyChanged("SelectedCardio");
}
}
}
}
Not sure where I am going wrong here but any pointers would be much appreciated.
Here is where I thought I was adding in the userControl to a content control bound proprty in my Main view model:
public void NewTemplateExecute()
{
TextHideTab = "Close";
NewTemplateType = ("New " + SelectedExercise + " Exercise Template");
//Set the message and lists based on the exercise selected plus adds the drop down control
switch (SelectedExercise)
{
case "Cardio":
///
//This is where I thought CardioVM was being added
///
NewTemplateText = "Please choose a cardio exercise from the drop down list to the left. You can then select the duration of the exercise and the intensity. To add another exercise please press the plus button in the right hand corner";
ExerciseDropDowns = new CardioVM(selectedDateLabel, #"Model\Repository\Local Data\CardioList.txt");
break;
case "Weights":
NewTemplateText = "Please select a exercise type. you can refine your exercises by body area. Then add the number of sets and the reps per set. Add as many exercises as you like - dont forget to set to total duration";
break;
case "HIIT":
NewTemplateText = "HIIT to add";
break;
}
Messenger.Default.Send("NewTemplate");
}
I had set the datacontext for CardioVM in my Mainwindow xaml as:
<DataTemplate DataType="{x:Type local:CardioVM}">
<view:UserControl1/>
</DataTemplate>
I presume I have made a mistake in the way that I have hooked up CaridoVM but couldn't seem to get it to databind unless I sent it through the VM locator
Thanks to nemesv - you were of course spot on. Removed the DataContext from my CardioVM and now just using DataTemplate set in Main view to bind the Cardio view to the ViewModel. I can now call a cardioVM with parameters from the Mainview and it populates my combobox as expected. Seems I nbeeded to touch up on some of the basics of MVVM
I have a DataGrid (dataGrid1) where records can be added and deleted.
Based on that dataGrid1, I want to make a new Grid with buttons in it based on ID and Types'. Cols will also have to given a DataSource of add dynamically, but that will be just while generating for the 1st time in Window_Loaded itself. Rows can be added/removed based on changes in dataGrid1. I want somethign like this :
On each Btn click, a new window will be opened for entry of the particular Type and for the particular ID. If the details are already entered, then the text of btn wil be "Update" else "Add".
What could be the best resource/control to perform this operations ? At present, I just did a Grid with 2 stable cols. Any ideas for the above to use Grid, DataGrid or something else. And adding/removing rows will be easy in which way and how.
Any help is appreciated.
Okay, let me try to take an example which is similar to your needs
Let's assume we use this class:
public class MyObject
{
public int MyID;
public string MyString;
public ICommand MyCommand;
}
And we are willing to display a DataGrid listing the ID, and having as a second column a Button, with the property MyString as content, which, when clicked, launches the ICommand MyCommand which opens in a new window whatever you want.
Here is what you should have on the View side:
<DataGrid ItemsSource="{Binding MyList}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding MyID}" />
<DataGridTemplateColumn Header="Buttons">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<Button Content="{Binding MyString}" Command="{Binding MyCommand}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
This will show a DataGrid taking all the content in an IEnumerable<MyObject> named 'MyList', and shows two columns as defined before.
Now if you need to define the command.
First, I recommend you read this introductory link to MVVM and take the RelayCommand class (that's what we're gonna use for your problem)
So, in your ViewModel, the one which defines the MyList, here is how you should define some of the useful objects:
public ObservableCollection<MyObject> MyList { get; set; }
// blah blah blah
public void InitializeMyList()
{
MyList = new ObservableCollection<MyObject>();
for (int i = 0; i < 5; i++)
{
MyList.Add(InitializeMyObject(i));
}
}
public MyObject InitializeMyObject(int i)
{
MyObject theObject = new MyObject();
theObject.MyID = i;
theObject.MyString = "The object " + i;
theObject.MyCommand = new RelayCommand(param =< this.ShowWindow(i));
return theObject
}
private void ShowWindow(int i)
{
// Just as an exammple, here I just show a MessageBox
MessageBox.Show("You clicked on object " + i + "!!!");
}
This should be enough to create whatever you want. As you can see, every Button will call a method (ShowWindow) which is defined to show your new window, do whatever you need inside. The RelayCommand is actually just here, as its name says, to relay the command fired by the button to a method which contains the execution logic.
And... I think that's all you need. Sorry for the late answer BTW
EDIT - generating columns manually/dynamically
The following code is part of a code I had to do when I had a similar problem.
My problem was, I needed to change the columns displayed every time a ComboBox's SelectedItem would change. So I put this in a SelectionChanged event handler.
I don't know where exactly do you need to generate your columns, but I'll give you a general example.
Assume your ItemsSource is an ObservableCollection<MyNewObject>
MyNewObject is the following:
public class MyNewObject
{
public IList<string> MyStrings { get; set; }
}
You should put somewhere in your code (should be when you need to generate the column) the following code, which is generating a number of columns equal to the length of the first MyNewObject from the list (note: this is in code-behind, and the DataGrid you're working on is named dataGrid)
ObservableCollection<MyNewObject> source = dataGrid.ItemsSource as ObservableCollection<MyNewObject>;
if (source == null || source.Count == 0)
{
return;
}
MyNewObject firstObject = source[0];
for(int i = 0; i < firstObject.MyStrings.Count; i++)
{
// Creates one column filled with buttons for each string
DataGridTemplateColumn columnToAdd = new DataGridTemplateColumn();
columnToAdd.Width = 110; // I set a manual width, but you can do whatever you want
columnToAdd.Header = "Header number " + i;
// Create the template with a Button inside, bound to the appropriate string
DataTemplate dataTemplate = new DataTemplate(typeof(Button));
FrameworkElementFactory buttonElement = new FrameworkElementFactory(typeof(Button));
Binding binding = new Binding("MyStrings[" + i + "]");
binding.Mode = BindingMode.TwoWay;
binding.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
buttonElement.SetBinding(Button.ContentProperty, binding);
// Do the same here for your command, or for whatever you want to do when the user clicks on this button
dataTemplate.VisualTree = buttonElement;
columnToAdd.CellTemplate = dataTemplate;
dataGrid.Columns.Add(columnToAdd);
}
This will create one column for each string found in the first object. Then, enhance it with whatever command or display trick you need!
I have one chm for my application which i want to attach with my application that is when user press F1 attached help with the project opens up.
I do not know of any in built support in WPF to display CHM files. What I do is add an InputGesture to connect F1 keystroke to Application.Help command and in the Windows CommandBindings add a handler for Application.Help Command. Here is a sample code:
<Window x:Class="WpfTestApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" >
<Window.InputBindings>
<KeyBinding Command="Help" Key="F1"/>
</Window.InputBindings>
<Window.CommandBindings>
<CommandBinding Command="ApplicationCommands.Help" Executed="HelpExecuted" />
</Window.CommandBindings>
<Grid>
</Grid>
Here's the handler code:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void HelpExecuted(object sender, ExecutedRoutedEventArgs e)
{
System.Diagnostics.Process.Start(#"C:\MyProjectPath\HelpFile.chm");
}
}
Using F1 Help (CHM format) With WPF
Based on this approach, I did the following so I could take advantage of an OnlineHelpViewModel I had that was managing the help through a RelayCommand. When F1 is pressed, with this approach, the RelayCommand on the viewmodel is invoked just as if ia ? button had been pushed. In other words,we bind F1 to the RelayCommand.
This example uses GalaSoft MvvmLight.
DependencyProperty on the MainWindow
public static DependencyProperty HelpCommandProperty = DependencyProperty.Register("HelpCommand",
typeof(RelayCommand<string>), typeof(WindowExt),
new PropertyMetadata(null));
public RelayCommand<string> HelpCommand
{
get
{
return (RelayCommand<string>)GetValue(HelpCommandProperty);
}
set
{
SetValue(HelpCommandProperty, value);
}
}
OK that holds the command
Now in the window loaded event or somewhere you like:
...
Binding b2 = new Binding();
b2.Source = ViewModelLocator.OnlineHelpViewModelStatic;
b2.Path = new PropertyPath("ShowApplicationHelpCommand");
b2.Mode = BindingMode.OneWay;
this.SetBinding(HelpCommandProperty, b2);
var kb = new KeyBinding();
kb.Key = Key.F1;
kb.Command = HelpCommand;
this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Help, HelpCommand_Executed));
OK that binds the command on the SOURCE viewmodel to this window.
Then a handler for the command on this window ( perhaps this can be inline somehow)
private void HelpCommand_Executed(object sender, ExecutedRoutedEventArgs e)
{
this.HelpCommand.Execute(HelpContextGuid);
}
and you now can call the single help command on OnlineHelpViewModel from anywhere, and it can be arbitrarily complicated too depending. Note that the DP HelpContextGuid is passed - it is up to the command to decide what to do with it but the RelayCommmand<string> wants an argument
The command itself looks like (on the SOURCE Viewmodel)
...
ShowApplicationHelpCommand = new RelayCommand<string>(
(h) => { ShowApplicationHelp(h); },
(h) => CanShowApplicationHelpCommand);
...
and method it invokes is whatever it takes to show the help,
In my case, I create a RadWindow and so on and populated it with XamlHelp using the BackSpin Software HelpLoader. The help file is generated from Word with Twister4Word.
All of this is particular to my application so you would probably do something else to make a help window. Here is the constructor:
public MigratorHelpWindow()
{
// create local resources for desingn mode, so Blend can see the viewmodels
if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
{
App.CreateStaticResourcesForDesigner(this);
}
InitializeComponent();
if (Application.Current.MainWindow != null)
{
var thm = ThemeManager.FromName(Application.Current.FindResource("TelerikGlobalTheme").ToString() ?? "Office_Blue");
StyleManager.SetTheme(this, thm);
}
// window configuration
MaxHeight = SystemParameters.WorkArea.Height;
MaxWidth = SystemParameters.WorkArea.Width;
Binding b = new Binding();
b.Source = ViewModelLocator.OnlineHelpViewModelStatic;
b.Path = new PropertyPath("ApplicationHelpFileName");
b.Mode = BindingMode.OneWay;
this.SetBinding(ApplicationHelpFileNameProperty, b);
if (String.IsNullOrEmpty(ApplicationHelpFileName))
{
UiHelpers.ShowError("No help file is available", true);
return;
}
// LOAD YOUR HELP HERE OR WHATEVER
// LOAD YOUR HELP HERE OR WHATEVER
// LOAD YOUR HELP HERE OR WHATEVER
HelpLoader.Load(ApplicationHelpFileName);
HelpLoader.Default.Owner = this;
HelpLoader.Default.HelpLayout = HelpLayout.Standard;
HelpLoader.Default.TocContainer = _mTOC;
HelpLoader.Default.IndexContainer = _mIndex;
HelpLoader.Default.TopicContainer = _mTopic;
HelpLoader.Default.SearchContainer = _mSearch;
HelpLoader.Default.FavoritesContainer = _mFavorites;
}
You can find the BackSpin Help Authoring tool here
http://www.backspinsoftware.com/site/Default.aspx
It generates compiled help from Word documents.
I'm using the WPF toolkit datagrid and in the past have always created entities for the grid to bind to, so for example a Contact Entity with Name, Address etc.
On the current app I'm working on the user may select from 50 tables and individually select the fields from the tables to generate a view.
Clearly here having an Entity to bind to will not work as the binding source will be dynamic.
Question is what do I do?
Thanks
I just blogged about how to dynamically create columns for a DataGrid based on a reusable model.
The best solution is to use Anonymous Types it works perfectly, see the following proof of concept:
<Window x:Class="MyProject.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:System="clr-namespace:System;assembly=mscorlib"
Title="MainWindow"
Height="136" Width="525"
Loaded="OnWindowLoaded">
<DataGrid ItemsSource="{Binding}">
</DataGrid>
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
namespace MyProject {
public partial class MainWindow : Window
{
public class Employee
{
public int Id { get; set; }
public string Code { get; set; }
public string Name { get; set; }
public int Job { get; set; }
public string Address { get; set; }
}
private ObservableCollection<Employee> _empCollection;
public MainWindow()
{
InitializeComponent();
}
private void OnWindowLoaded(object sender, RoutedEventArgs e)
{
// Generate test data
_empCollection =
new ObservableCollection<Employee>
{
new Employee {Id = 234, Code = "E041", Name = "Employee1", Job = 1, Address = "..."},
new Employee {Id = 245, Code = "E701", Name = "Employee2", Job = 3, Address = "..."},
new Employee {Id = 728, Code = "E001", Name = "Employee3", Job = 9, Address = "..."},
new Employee {Id = 663, Code = "E051", Name = "Employee4", Job = 7, Address = "..."},
};
DataContext =
(from i in _empCollection
select new {i.Code, i.Name, i.Address}).ToList();
}
}
}
One approach would be to create a collection of objects, and give each object a custom TypeDescriptor.
When the grid is auto generating columns, it uses reflection over your class - e.g., Customer, and discovers its properties - e.g., FirstName, Balance, etc.
But that's not entirely true. WPF doesn't do the work itself - it asks a TypeDescriptor. And you can implement your own TypeDescriptor, so you can pretend to have properties that don't actually exist. Or in your case, pretend not to have properties that do exist.
You can leave binding source as it is, however you can filter DataGrid's columns based on user's preferences of what he/she needs to hide or see.
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]):
Fanta
Pepsi
Coca Cola
Sprite
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");
}
}
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.
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.
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