I have spent considerable amount of time investigating this problem. Any help would be greatly appreciated.
I have a WPF ComboBox declared like this.
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Button Click="Button_Click">Click Me</Button>
<ComboBox ItemsSource="{Binding ListOfValues}" SelectedItem="{Binding MySelectedItem}" Grid.Row="1">
</ComboBox>
<CheckBox IsChecked="{Binding IsValueChecked}" Grid.Row="2"></CheckBox>
</Grid>
In my code behind, i have these properties and i am implementing the INotifyPropertyChanged
public Window1()
{
InitializeComponent();
ListOfValues = new List<string>();
ListOfValues.Add("apple");
ListOfValues.Add("ball");
ListOfValues.Add("cat");
ListOfValues.Add("dog");
MySelectedItem = "cat";
IsValueChecked = true;
}
public List<string> ListOfValues
{
get
{
return _listOfValues;
}
set
{
_listOfValues = value;
OnPropertyChanged("ListOfValues");
}
}
public string MySelectedItem
{
get
{
return _selectedValueString;
}
set
{
_selectedValueString = value;
OnPropertyChanged("MySelectedItem");
}
}
public bool IsValueChecked
{
get
{
return _isVlaueChanged;
}
set
{
_isVlaueChanged = value;
OnPropertyChanged("IsValueChecked");
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MySelectedItem = "dog";
IsValueChecked = !IsValueChecked;
}
The button click event changes the MySelectedItem which is bound to the SelectedItem property of the combobox. But upon the button click nothing gets selected in the combobox. I dont understand why. This happens even if i set explicitly Mode=TwoWay. Please suggest. Note that my datacontext is set to self, so i have confirmed that data binding is happening properly by adding a checkbox
EDIT: Note that this happens in a sample WPF project. But my original project where i want this to work is a winforms app. I am using the elementhost to embed my wpf control. Is that making a difference?
The selected item needs to be set to an object in the list you have it bound to. settings it to a string with a matching value won't work. So try this:
foreach(string animal in ListOfValues)
{
if( animal == "dog")
this.MySelectedItem = animal;
}
I tried to reproduce your problem and I have some questions. Can you please show me your implementation of OnPropertyChanged? When I have a look at the MSDN (http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.onpropertychanged.aspx) this function requires a DependencyPropertyChangedEventArgs as the first parameter, not a string. And in addition, OnPropertyChanged is for notifying about changes in Dependency Properties, not for normal properties.
So I think you overloaded that method to support INotifyPropertyChanged, right?
I tried to implement a working example, this is the result:
public partial class TestWindow2 : Window, INotifyPropertyChanged
{
public TestWindow2()
{
InitializeComponent();
ListOfValues = new List<string> { "apple", "ball", "cat", "dog" };
MySelectedItem = "cat";
IsValueChecked = true;
this.DataContext = this;
}
...
public string MySelectedItem
{
get
{
return _selectedValueString;
}
set
{
_selectedValueString = value;
RaisePropertyChanged("MySelectedItem");
}
}
...
private void Button_Click(object sender, RoutedEventArgs e)
{
MySelectedItem = "dog";
IsValueChecked = !IsValueChecked;
}
private void RaisePropertyChanged(String name)
{
if( this.PropertyChanged != null ) this.PropertyChanged(this, new PropertyChangedEventArgs(name));
}
public event PropertyChangedEventHandler PropertyChanged;
}
Works perfectly for me. When I click the button, dog becoms the selected item in the combo box and the checkbox toggles its state.
If your items are a reference type (and you are just using string for an example), check that the Equals() method is returning what you expect. You might need to override the Equals method (eg this.ID ==other.ID or something like that) to get the correct behavior.
Related
I try to create a DataGrid for WPF / MVVM which allows to manually select one ore more items from ViewModel code.
As usual the DataGrid should be able to bind its ItemsSource to a List / ObservableCollection. The new part is that it should maintain another bindable list, the SelectedItemsList. Each item added to this list should immediately be selected in the DataGrid.
I found this solution on Stackoverflow: There the DataGrid is extended to hold a Property / DependencyProperty for the SelectedItemsList:
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.SelectedItemsList = this.SelectedItems;
}
public IList SelectedItemsList
{
get { return (IList)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register("SelectedItemsList",
typeof(IList),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
In the View/XAML this property is bound to the ViewModel:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ucc:CustomDataGrid Grid.Row="0"
ItemsSource="{Binding DataGridItems}"
SelectionMode="Extended"
AlternatingRowBackground="Beige"
SelectionUnit="FullRow"
IsReadOnly="True"
SelectedItemsList="{Binding DataGridSelectedItems,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />
<Button Grid.Row="1"
Margin="5"
HorizontalAlignment="Center"
Content="Select some rows"
Command="{Binding CmdSelectSomeRows}"/>
</Grid>
The ViewModel also implements the command CmdSelectSomeRows which selects some rows for testing. The ViewModel of the test application looks like this:
public class CustomDataGridViewModel : ObservableObject
{
public IList DataGridSelectedItems
{
get { return dataGridSelectedItems; }
set
{
dataGridSelectedItems = value;
OnPropertyChanged(nameof(DataGridSelectedItems));
}
}
public ICommand CmdSelectSomeRows { get; }
public ObservableCollection<ExamplePersonModel> DataGridItems { get; private set; }
public CustomDataGridViewModel()
{
// Create some example items
DataGridItems = new ObservableCollection<ExamplePersonModel>();
for (int i = 0; i < 10; i++)
{
DataGridItems.Add(new ExamplePersonModel
{
Name = $"Test {i}",
Age = i * 22
});
}
CmdSelectSomeRows = new RelayCommand(() =>
{
if (DataGridSelectedItems == null)
{
DataGridSelectedItems = new ObservableCollection<ExamplePersonModel>();
}
else
{
DataGridSelectedItems.Clear();
}
DataGridSelectedItems.Add(DataGridItems[0]);
DataGridSelectedItems.Add(DataGridItems[1]);
DataGridSelectedItems.Add(DataGridItems[4]);
DataGridSelectedItems.Add(DataGridItems[6]);
}, () => true);
}
private IList dataGridSelectedItems = new ArrayList();
}
This works, but only partially: After application start when items are added to the SelectedItemsList from ViewModel, they are not displayed as selected rows in the DataGrid. To get it to work I must first select some rows with the mouse. When I then add items to the SelectedItemsList from ViewModel these are displayed selected – as I want it.
How can I achieve this without having to first select some rows with the mouse?
You should subscribe to the Loaded event in your CustomDataGrid and initialize the SelectedItems of the Grid (since you never entered the SelectionChangedEvent, there is no link between the SelectedItemsList and the SelectedItems of your DataGrid.
private bool isSelectionInitialization = false;
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
this.isSelectionInitialization = true;
foreach (var item in this.SelectedItemsList)
{
this.SelectedItems.Clear();
this.SelectedItems.Add(item);
}
this.isSelectionInitialization = false;
}
and the SelectionChanged event handler has to be modified like this:
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!this.isSelectionInitialization)
{
this.SelectedItemsList = this.SelectedItems;
}
else
{
//Initialization from the ViewModel
}
}
Note that while this will fix your problem, this won't be a true synchronization as it will only copy the items from the ViewModel at the beginning.
If you need to change the items in the ViewModel at a later time and have it reflected in the selection let me know and I will edit my answer.
Edit: Solution to have a "true" synchronization
I created a class inheriting from DataGrid like you did.
You will need to add the using
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Windows;
using System.Windows.Controls;
public class CustomDataGrid : DataGrid
{
public CustomDataGrid()
{
this.SelectionChanged += CustomDataGrid_SelectionChanged;
this.Loaded += CustomDataGrid_Loaded;
}
private void CustomDataGrid_Loaded(object sender, RoutedEventArgs e)
{
//Can't do it in the constructor as the bound values won't be initialized
//If it is expected for the bound collection to be null initially, you could subscribe to the change of the
//dependency in order to subscribe to the collectionChanged event on the first non null value
this.SelectedItemsList.CollectionChanged += SelectedItemsList_CollectionChanged;
//We call the update in case we have already some items in the VM collection
this.UpdateUIWithSelectedItemsFromVm();
if(this.SelectedItems.Count != 0)
{
//Otherwise the items won't be as visible unless you change the style (this part is not required)
this.Focus();
}
else
{
//No focus
}
}
private void SelectedItemsList_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
this.UpdateUIWithSelectedItemsFromVm();
}
private void UpdateUIWithSelectedItemsFromVm()
{
if (!this.isSelectionChangeFromUI)
{
this.isSelectionChangeFromViewModel = true;
this.SelectedItems.Clear();
if (this.SelectedItemsList == null)
{
//Nothing to do, we just cleared all the selections
}
else
{
if (this.SelectedItemsList is IList iListFromVM)
foreach (var item in iListFromVM)
{
this.SelectedItems.Add(item);
}
}
this.isSelectionChangeFromViewModel = false;
}
else
{
//Nothing to do, the change is coming from the SelectionChanged event
}
}
private void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
//If your collection allow suspension of notifications, it would be a good idea to add a check here in order to use it
if(!this.isSelectionChangeFromViewModel)
{
this.isSelectionChangeFromUI = true;
if (this.SelectedItemsList is IList iListFromVM)
{
iListFromVM.Clear();
foreach (var item in SelectedItems)
{
iListFromVM.Add(item);
}
}
else
{
throw new InvalidOperationException("The bound collection must inherit from IList");
}
this.isSelectionChangeFromUI = false;
}
else
{
//Nothing to do, the change is comming from the bound collection so no need to update it
}
}
private bool isSelectionChangeFromUI = false;
private bool isSelectionChangeFromViewModel = false;
public INotifyCollectionChanged SelectedItemsList
{
get { return (INotifyCollectionChanged)GetValue(SelectedItemsListProperty); }
set { SetValue(SelectedItemsListProperty, value); }
}
public static readonly DependencyProperty SelectedItemsListProperty =
DependencyProperty.Register(nameof(SelectedItemsList),
typeof(INotifyCollectionChanged),
typeof(CustomDataGrid),
new PropertyMetadata(null));
}
You will have to initialize the DataGridSelectedItems earlier or you there will be a null exception when trying to subscribe to the collectionChanged event.
/// <summary>
/// I removed the notify property changed from your example as it probably isn't necessary unless you really intended to create a new Collection at some point instead of just clearing the items
/// (In this case you will have to adapt the code for the synchronization of CustomDataGrid so that it subscribe to the collectionChanged event of the new collection)
/// </summary>
public ObservableCollection<ExamplePersonModel> DataGridSelectedItems { get; set; } = new ObservableCollection<ExamplePersonModel>();
I didn't try all the edge cases but this should give you a good start and I added some directions as to how to improve it. Let me know if some parts of the code aren't clear and I will try to add some comments.
WPF is not normally my area, so I am a bit of a newbie, and I am having a bit of trouble figuring out how to achieve something in WPF which was a piece of cake in WinForms. I can't seem to find either the right thread in this forum or the right YouTube tutorial that leads me towards the answer. I am having problems getting a simple DataBinding to a WPF TextBox working correctly. The behaviour that I am trying to achieve is that any changes made to the TextBox are immediately reflected in the source class DataSet. It's a simple display/edit scenario and I'm sure there is a very simple answer.
This is how I would have done it in WinForms....
Form code:
public partial class Form1 : Form
{
private DATARECORD CURRENTUSER;
public Form1()
{
InitializeComponent();
CURRENTUSER = new DATARECORD(#"Data Source=C:\Users\rr187718\Documents\Personal\Programming\DynamicBackup\DynamicBackup\bin\Debug\Data\dbData.sdf");
CURRENTUSER.FncBind(CtlCopiesToKeep, "Value", "tblUser.CopiesToKeep");
}
//Test code to display the value in the DataSet
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show(CURRENTUSER.copiesToKeep.ToString());
}
}
Class code:
public class DATARECORD
{
private string ConnectionString;
private DataSet CurrentRecord;
public int copiesToKeep { get { return Int32.Parse(CurrentRecord.Tables["tblUser"].Rows[0]["CopiesToKeep"].ToString()); } }
public DATARECORD(string connectionString)
{
ConnectionString = connectionString;
CurrentRecord = new DataSet();
SQL SQL = new SQL(2);
DataTable userTable = SQL.fncSelectAsTable(ConnectionString, "tblUser", "USERID=2");
userTable.TableName = "tblUser";
CurrentRecord.Tables.Add(userTable);
userTable.Dispose();
}
public void FncBind(Control c, string type, string field)
{
c.DataBindings.Add(type, CurrentRecord, field, true, DataSourceUpdateMode.OnPropertyChanged);
}
}
I then just have simple TextBox on the main Form called "CtlCopiesToKeep" and a "test" button.
Does anyone know of a nice, simple, example that can show how to do this?
Many thanks in advance,
Dave
EDIT:
Hello Noel. Many thanks for taking the time to explain all that. I have put it altogether, but something seems to be wrong with the binding, because when I change the value in the TextBox it does not update the DataSet. Here is the code and the XAML. If anyone can point me in the right direction then it would be much appreciated.
UPDATED Main code
public partial class MainWindow : Window
{
public DATARECORD SELECTEDUSER;
private string ConnectionString = #"Data Source=C:\Users\rr187718\Documents\Personal\Programming\DynamicBackup\DynamicBackup\bin\Debug\Data\dbData.sdf";
public MainWindow()
{
InitializeComponent();
SELECTEDUSER = new DATARECORD(ConnectionString);
GrdMain.DataContext = SELECTEDUSER;
}
private void button1_Click(object sender, RoutedEventArgs e)
{
SELECTEDUSER.fncShowVals("BasePath");
}
}
UPDATED Class code
public class DATARECORD : INotifyPropertyChanged
{
private string ConnectionString;
private DataSet currentRecord = new DataSet();
private string BasePath = null;
public string basePath
{
get
{
return currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString() ;
}
set
{
BasePath = value;
OnPropertyChanged("BasePath");
}
}
public event PropertyChangedEventHandler PropertyChanged;
public DATARECORD(string connectionString)
{
ConnectionString = connectionString;
SQL SQL = new SQL(ConnectionString, SQLVersion.CE);
DataTable storageTable = SQL.fncSelectAsTable(ConnectionString, "tblStorage", "USERID=2");
storageTable.TableName = "tblStorage";
currentRecord.Tables.Add(storageTable);
storageTable.Dispose();
}
public void fncShowVals(string test)
{
MessageBox.Show(currentRecord.Tables["tblStorage"].Rows[0][test].ToString());
}
protected void OnPropertyChanged(string value)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(value));
}
}
}
XAML for TextBox
<Window x:Class="WpfBind.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">
<Grid Name="GrdMain">
<TextBox Text="{Binding basePath, Mode=TwoWay, UpdateSourceTrigger =PropertyChanged}" Height="23" HorizontalAlignment="Left" Margin="124,70,0,0" Name="CtlBaseFolder" VerticalAlignment="Top" Width="120" />
<Label Content="BaseFolder" Height="28" HorizontalAlignment="Left" Margin="41,69,0,0" Name="label2" VerticalAlignment="Top" />
<Button Content="Button" Height="23" HorizontalAlignment="Left" Margin="263,142,0,0" Name="button1" VerticalAlignment="Top" Width="75" Click="button1_Click" />
</Grid>
UPDATE 02/04/2015
I now have this, but I don't understand how it references the DataSet? This code produces a blank textbox and if the value is changed it doesn't update the DataSet:
`private string ___basePath = null;
protected string _basePath
{
get
{
return ___basePath;
}
set
{
___basePath = value;
OnPropertyChanged("basePath");
}
}
public string basePath
{ //<- Bind to this property
get
{
return ___basePath;
}
set
{
_basePath = value;
}
}`
The underlying DataSet value is stored here:
currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
Many thanks in advance, Dave.
UPDATE - 02/04/2015 - 2
Hello Noel, I have applied your code, but it's still not working unfortunately (the DataSet does not reflect the changes in the TextBox if I click on the "test" button). Here is the whole code. I massively appreciate your time on this by the way, thanks so much!
public partial class MainWindow : Window
{
private string ConnectionString = #"Data Source=C:\Users\rr187718\Documents\Personal\Programming\DynamicBackup\DynamicBackup\bin\Debug\Data\dbData.sdf";
private readonly DATARECORD _data = null;
public DATARECORD Data
{
get
{
return _data;
}
}
public MainWindow()
{
InitializeComponent();
_data = new DATARECORD(ConnectionString);
DataContext = Data; //All controls connected to this class will now look for their value in 'Data' (DataContext inherits and must be a property because you can only bind to properties)
}
private void button1_Click(object sender, RoutedEventArgs e)
{
Data.fncShowVals("BasePath");
}
}
public class DATARECORD : INotifyPropertyChanged
{
private string ConnectionString;
private DataSet currentRecord = new DataSet();
private string ___basePath = null;
private string _basePath
{
get
{
if (___basePath == null)
{
//We only access the currentRecord if we did not yet stored the value
// otherwise it would read the currentRecord every time you type a char
// in the textbox.
// Also: Pay attention to multiple possible NullReferenceExceptions and IndexOutOfBoundsExceptions
___basePath = currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
}
return (___basePath == String.Empty) ? null : ___basePath;
}
set
{
___basePath = (value == null) ? String.Empty : value;
NotifyPropertyChanged("BasePath");
}
}
protected void PushBasePathToDataBase()
{
//Save the value of ___basePath to the database
}
public string BasePath
{ //The Binding recieves/sets the Data from/to this property
get
{
return _basePath;
}
set
{
_basePath = value;
}
}
public event PropertyChangedEventHandler PropertyChanged;
public DATARECORD(string connectionString)
{
ConnectionString = connectionString;
SQL SQL = new SQL(ConnectionString, SQLVersion.CE);
DataTable storageTable = SQL.fncSelectAsTable(ConnectionString, "tblStorage", "USERID=2");
storageTable.TableName = "tblStorage";
currentRecord.Tables.Add(storageTable);
storageTable.Dispose();
___basePath = currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
}
public void fncShowVals(string test)
{
MessageBox.Show(currentRecord.Tables["tblStorage"].Rows[0][test].ToString());
}
protected void NotifyPropertyChanged(string PropertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
It is great that you are using a binding to seperate data from the visuals. Since that was not really possible in winforms. In order for a binding to work you must do the following:
The textBox must have its DataContext set to the instance of a class which holds the binding-value. DataContext = MyDataInstance; You can set that on the textbox itself or on any parent.
The value as well as the DataContext you want to bind must be a public property. F.e:
private string _name = null;
public string Name{
get{
return _name;
}
set{
_name = value;
NotifyPropertyChanged("Name");
}
}
The Data Class must implement INotifyPropertyChanged
If that is all set up you can write your textbox in xaml:
<TextBlock Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
This Binding binds to the Name property of the instance specified in DataContext. It can retrieve the value from the property and it can write the data to it.
It recieves Data when you call NotifyPropertyChanged("Name"); in your DataClass
It writes Data when the property of the control changes (Requires Mode set to TwoWay and the UpdateSourceTrigger to PropertyChanged)
EDIT (regarding your additional content)
I noticed that you wanted to notify about your private field named "BasePath".
You must notify the property "basePath" and not the field behind it.
That is why I recommend a strict naming convention.
I do name private and protected fields like _privateOrProtected (1 underscore).
I name private or protected fields accessed by bindings properties like ___someData (3 underscores) and the binding property like SomeData. The reason is, that you usually don't want to set the private field directly except from the binding propertie's setter. Setting it directly would not call the NotifyPropertyChanged(); which obviously isn't what you want in almost all cases. And if you keep the 3 underscores throughout your app - everyone familliar with bindings should quickly understand the meaning.
For more complex data you might have a binding property accessing a private/protected property accessing a private field. I would solve it like this: SomeData, _someData, ___someData. You just have to make it clear wich properties or fields can be set in order to update the binding otherwise someone might change the value of ___someData and wonder why the binding isn't updating.
Since this is a quite important point in every WPF app I really want you to understand it. Here is an example for the stuff above:
private bool ___thisIsAwesome = true;
protected bool _thisIsAwesome{
get{
return ___thisIsAwesome;
}
set{
___thisIsAwesome = value;
NotifyPropertyChanged("ThisIsAwesome");
}
}
public bool ThisIsAwesome{ //<- Bind to this property
get{
return ___thisIsAwesome;
}
/*set{
_thisIsAwesome = value;
} NOTE: The setter is not accessable from outside of this class
because nobody can tell me that this is not awesome - it just is.
However I still want to be able to set the property correctly
from within my class (in case I change my mind), that is why I
added the protected property.
If you omit a getter/setter like this one make sure your
<br>Binding Mode</b> does not try to access the omited accessors.
Also check the output window too find possible binding errors
which never throw exceptions.
*/
}
In this code you should now recognize that setting ThisIsAwesome and _thisIsAwesome will both update the binding. But beware of setting ___thisIsAwesome because it won't update the Binding. The setter of ThisIsAwesome is currently not available (whatever reason) and that's why I added the protected property. Do you understand what I want to achieve with that?
EDIT2 (because your code still doesn't work)
public partial class MainWindow : Window {
private readonly MyData _data = null;
public MyData Data{
get{
return _data;
}
}
public MainWindow() {
_data = new MyData();
DataContext = Data; //All controls connected to this class will now look for their value in 'Data' (DataContext inherits and must be a property because you can only bind to properties)
}
}
public class MyData : INotifyPropertyChanged {
private string ___basePath = null;
private string _basePath {
get {
if (___basePath == null) {
//We only access the currentRecord if we did not yet stored the value
// otherwise it would read the currentRecord every time you type a char
// in the textbox.
// Also: Pay attention to multiple possible NullReferenceExceptions and IndexOutOfBoundsExceptions
___basePath = currentRecord.Tables["tblStorage"].Rows[0]["BasePath"].ToString();
}
return (___basePath == String.Empty) ? null : ___basePath;
}
set {
___basePath = (value == null) ? String.Empty : value;
NotifyPropertyChanged("BasePath");
}
}
protected void PushBasePathToDataBase() {
//Save the value of ___basePath to the database
}
public string BasePath{ //The Binding recieves/sets the Data from/to this property
get{
return _basePath;
}
set{
_basePath = value;
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string PropertyName){
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
#endregion INotifyPropertyChanged
}
And finally the textbox in your MainWindow's xaml:
<TextBlock Text="{Binding BasePath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
Scenario: In a Silverlight 4 MVVM project, we have a ListBox control containing items, the selected item is two-way-bound to the appropriate property in the ViewModel. Another control (for example reasons, I've stripped it down to a single TextBox) is data bound to the selected item's content. The value should update on leave/focus lost.
Problem: When the value in the TextBox is changed and we leave that TextBox by pressing the Tab key, everything works as desired - the value is updated. However, if the user clicks on a different item in the ListBox, then the SelectedItem setter is fired before the content of TextBox setter is fired, leaving no chance to handle the user input.
You can see in debugger, when adding breakpoints to the property setters, that the new ListView selection is applied first, before the TextBox update is processed.
Desired behavior: We need to know that the currently selected item was modified before the user has selected another item. It's not desired to have a custom update trigger which would notify on each key press (we know that's possible).
Can you help?
Code (a very simple example):
ViewModel
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class ItemViewModel : ViewModelBase
{
private string _content;
public ItemViewModel(string initContent)
{
_content = initContent;
}
public string Content
{
get
{
return _content;
}
set
{
if (_content != value)
{
_content = value;
OnPropertyChanged("Content");
}
}
}
}
public class MainViewModel : ViewModelBase
{
private ObservableCollection<ItemViewModel> _items =
new ObservableCollection<ItemViewModel>();
private ItemViewModel _selectedViewModel;
public ObservableCollection<ItemViewModel> Items
{
get
{
return _items;
}
}
public ItemViewModel SelectedItem
{
get
{
return _selectedViewModel;
}
set
{
if (_selectedViewModel != value)
{
_selectedViewModel = value;
OnPropertyChanged("SelectedItem");
}
}
}
}
XAML
<Grid x:Name="LayoutRoot" Background="White">
<ListBox Height="100"
HorizontalAlignment="Left"
Margin="12,12,0,0"
VerticalAlignment="Top"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
DisplayMemberPath="Content"
Width="220" />
<TextBox Height="23"
HorizontalAlignment="Left"
Margin="12,118,0,0"
Text="{Binding SelectedItem.Content, Mode=TwoWay}"
VerticalAlignment="Top"
Width="220" />
</Grid>
XAML Code Behind
public MvvmTestView()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MvvmTestView_Loaded);
}
void MvvmTestView_Loaded(object sender, RoutedEventArgs e)
{
MainViewModel viewModel = new MainViewModel();
viewModel.Items.Add(new ItemViewModel("Hello StackOverflow"));
viewModel.Items.Add(new ItemViewModel("Thanks to Community"));
DataContext = viewModel;
}
UPDATE 1
I present a self designed solution for you to check out, which will be probably be the accepted one, I still want to encourage you to make comments and give your hints. Thanks.
You could add a behavior to your textbox to updated the binding every time the text is changed in the textbox. Maybe this solved your problems.
Here´s the code for the Behavior class:
public class UpdateTextBindingOnPropertyChanged : Behavior<TextBox> {
// Fields
private BindingExpression expression;
// Methods
protected override void OnAttached() {
base.OnAttached();
this.expression = base.AssociatedObject.GetBindingExpression(TextBox.TextProperty);
base.AssociatedObject.TextChanged+= OnTextChanged;
}
protected override void OnDetaching() {
base.OnDetaching();
base.AssociatedObject.TextChanged-= OnTextChanged;
this.expression = null;
}
private void OnTextChanged(object sender, EventArgs args) {
this.expression.UpdateSource();
}
}
Heres the XAML:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="Namespace of the class where UpdateTextBindingOnPropertyChanged is defined"
<TextBox Text="{Binding SelectedItem.Content, Mode=TwoWay}">
<i:Interaction.Behaviors>
<local:UpdateTextBindingOnPropertyChanged />
</i:Interaction.Behaviors>
</TextBox >
This is one solution we currently came up with. It has the advantage that it separates different tasks to the appropriate layer. For example, the View enforces an update of the binding, while the ViewModel tells the View to do so. Another advantage is that its handled synchronously, which would for example allow to check the content right before switching away, and the call-stack remains unchanged without raising "External Code" (Going over Dispatcher or even DispatcherTimer would do so) which is better for maintenance and flow control. A disadvantage is the new Event which has to be bound and handled (and finally unbound. I present an anonymous handler only for example reasons).
How to get there?
In ViewModelBase, implement a new ForceBindingUpdate event:
public abstract class ViewModelBase : INotifyPropertyChanged
{
// ----- leave everything from original code ------
public event EventHandler ForceBindingUpdate;
protected void OnForceBindingUpdate()
{
var handler = ForceBindingUpdate;
if (handler != null)
handler(this, EventArgs.Empty);
}
}
In MainViewModel, update the setter of the SelectedItem property:
set // of SelectedItem Property
{
if (_selectedViewModel != value)
{
// Ensure Data Update - the new part
OnForceBindingUpdate();
// Old stuff
_selectedViewModel = value;
OnPropertyChanged("SelectedItem");
}
}
Update the MvvmTestView Code Behind to implement the new event:
void MvvmTestView_Loaded(object sender, RoutedEventArgs e)
{
// remains unchanged
Mvvm.MainViewModel viewModel = new Mvvm.MainViewModel();
viewModel.Items.Add(new Mvvm.ItemViewModel("Hello StackOverflow"));
viewModel.Items.Add(new Mvvm.ItemViewModel("Thanks to Community"));
// Ensure Data Update by rebinding the content property - the new part
viewModel.ForceBindingUpdate += (s, a) =>
{
var expr = ContentTextBox.GetBindingExpression(TextBox.TextProperty);
expr.UpdateSource();
};
// remains unchanged
DataContext = viewModel;
}
Last but not least, the minimal XAML Update: Give the TextBox a name by adding x:Name="ContentTextBox" Attribute to the TextBoxs XAML.
Done.
Actually, I don't know if this is the cleanest solution, but it gets close to what we had in mind.
Maybe you could handle TextBox LostFocus then (instead of listening to every key press)?
Other idea would be to keep a proxy property on the ViewModel instead of directly binding to SelectedItem.Content and writing some code to make sure the item is updated.
Solution №1
public class LazyTextBox: TextBox
{
//bind to that property instead..
public string LazyText
{
get { return (string)GetValue(LazyTextProperty); }
set { SetValue(LazyTextProperty, value); }
}
public static readonly DependencyProperty LazyTextProperty =
DependencyProperty.Register("LazyText", typeof(string), typeof(LazyTextBox),
new PropertyMetadata(null));
//call this method when it's really nessasary...
public void EnsureThatLazyTextEqualText()
{
if (this.Text != this.LazyText)
{
this.LazyText = this.Text;
}
}
}
Solution №2 (works as magic :) )
public class MainViewModel : ViewModelBase
{
private ObservableCollection<ItemViewModel> _items =
new ObservableCollection<ItemViewModel>();
private ItemViewModel _selectedViewModel;
public ObservableCollection<ItemViewModel> Items { get { return _items; } }
public ItemViewModel SelectedItem
{
get { return _selectedViewModel; }
set
{
if (_selectedViewModel != value)
{
if (SelectedItem != null)
{
SelectedItem.Content = SelectedItem.Content;
}
_selectedViewModel = value;
// A little delay make no harm :)
var t = new DispatcherTimer();
t.Interval = TimeSpan.FromSeconds(0.1);
t.Tick += new EventHandler(t_Tick);
t.Start();
}
}
}
void t_Tick(object sender, EventArgs e)
{
OnPropertyChanged("SelectedItem");
(sender as DispatcherTimer).Stop();
}
}
I know that in MVVM we do not want to put code in code behind. But in this instance it hurts nothing as it is entirely maintained in the UI and SOP is maintained.
By putting a ghost element to take focus we can swap the focus back in forth forcing
the text box to commit its contents. So in the code behind we take care of the focus wiggle.
But yet we still are using a relay command Update Command to execute the save. So the order is good as the Click event fires wiggling the view. And then the relay command UpdateCommand will fire and the textbox is committed and ready for update.
<MenuItem Header="_Save"
Command="{Binding UpdateCommand}" Click="MenuItem_Click">
</MenuItem>
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
UIElement elem = Keyboard.FocusedElement as UIElement;
Keyboard.Focus(ghost);
Keyboard.Focus(elem);
}
Solution #3
public abstract class ViewModelBase : INotifyPropertyChanged
{
private List<string> _propNameList = new List<string>();
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
_propNameList.Add(propertyName);
var t = new DispatcherTimer();
t.Interval = TimeSpan.FromSeconds(0);
t.Tick += new EventHandler(t_Tick);
t.Start();
}
void t_Tick(object sender, EventArgs e)
{
if (_propNameList.Count > 0)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(_propNameList[0]));
_propNameList.Remove(_propNameList[0]);
}
}
}
PS: it's the same timer.. but this solution is more generic..
I have a combo box defined as such
<ComboBox Name="RoomDropDown" Visibility="{Binding Path=RoomDropDownVisible,Mode=OneWay,Converter={StaticResource BoolVisibilityConvertor}}"
ItemsSource="{Binding Path=RoomList,Mode=OneWay}" DisplayMemberPath="display" SelectedValuePath="display" SelectedValue="{Binding Path=Room,Mode=TwoWay}"/>
There are 2 properties defined in the ViewModel, RoomList which is List and the Room property which is a string.
First time when i run the app everything works fine, and the Drop Down gets the correct values as well as the correct values is selected. However on a certain conditions the RoomList property is changed to a different source & the Room property is also changed. The problem that is now happening is the Combo Box is showing the correct values but the selected value is not getting selected. Worse, we can live with that, but the setter is also not firing when the value is manually changed in the DropDown.
Any pointers on what is going wrong here?
Followup:
Don't think I managed to get the exact problem across, here is some sample code that I wanted to add to illustrate the problem:
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel VerticalAlignment="Center" Width="100">
<ComboBox Name="TestBox" Height="20" Width="100" ItemsSource="{Binding Path=ComboSource}" DisplayMemberPath="display" SelectedValuePath="code"
SelectedValue="{Binding Path=ComboSelection,Mode=TwoWay}"/>
<Button Content="Click Here" Click="Button_Click" />
</StackPanel>
</Grid>
Code:-
public MainPage()
{
InitializeComponent();
this.Loaded += (s, e) =>
{
var temp = new List<Binding>();
temp.Add(new Binding() { code = "1", display = "One" });
temp.Add(new Binding() { code = "2", display = "Two" });
this.ComboSource = temp;
this.ComboSelection = "1";
this.DataContext = this;
};
}
private static readonly DependencyProperty ComboSelectionProperty =
DependencyProperty.Register("ComboSelectionProperty", typeof(string), typeof(MainPage), new PropertyMetadata(null));
public string ComboSelection
{
get { return (string)GetValue(ComboSelectionProperty); }
set
{
SetValue(ComboSelectionProperty, value);
this.RaisePropertyChanged("ComboSelection");
}
}
private static readonly DependencyProperty ComboSourceProperty =
DependencyProperty.Register("ComboSourceProperty", typeof(List<Binding>), typeof(MainPage), new PropertyMetadata(null));
public List<Binding> ComboSource
{
get
{
return (List<Binding>)GetValue(ComboSourceProperty);
}
set
{
SetValue(ComboSourceProperty, value);
this.RaisePropertyChanged("ComboSource");
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
var temp = new List<Binding>();
temp.Add(new Binding() { code = "3", display = "Three" });
temp.Add(new Binding() { code = "4", display = "Four" });
this.ComboSource = temp;
this.ComboSelection = "3";
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
public class Binding
{
public string code {get; set;}
public string display { get; set; }
}
Not strictly MVVM, but to explain the problem, when the button click event is fired, the Combosource is changed with a new selection being made, however that selection does not bind and the problem i mentioned above starts happening.
Not sure about your actual sourcecode, but in your example your dependency registrations are not quite right.
Should have been:
DependencyProperty.Register("ComboSelection"
and
DependencyProperty.Register("ComboSource"
not "xxxProperty". Then it all fires the change correctly.
Basically you are registering the dependency object itself, instead of the get/set property methods. The parameter documentation/naming may seem a little misleading.
*Note: As per Dan Bryant's comment - the INotifyPropertyChange stuff is also not needed and I ripped it out of your sample code (I can assure you it works perfectly fine without it).
hello i'm building a wpf app with data grids,
the pattern is model view view model.
all og my screens contains a contentcontrol, and i just assign him the view model, that have a suitable data template,
anyway, my problem is with combo box column, the data context is the presented entity, and i need it to be the view model.
whats the best solution?
I'm using another datagrid, but it might be similar. The way i did it was like that:
in the XAML, i defined an ObjectDataProvider in the resources:
<ObjectDataProvider x:Key="VM" ObjectInstance="{x:Null}" x:Name="vm"/>
then after assigning the DataContext (either the constructor or the DataContextChanged event), i did this:
(this.Resources["VM"] as ObjectDataProvider).ObjectInstance = this.DataContext;
In the Combobox xaml, i used that as binding source:
ItemsSource="{Binding Source={StaticResource VM}, Path=SomeItems, Mode=OneWay}"
Not sure if it works for the microsoft datagrid, but i guess it's worth a try.
this is how I used ViewModel with ComboBoxes, the DataContext is the ViewModel, not the underlying entity (List<Person>).
ViewModel (Person is a Simple class with Name and Age):
public class PeopleViewModel : INotifyPropertyChanged
{
private List<Person> _peopleList;
private Person _selectedPerson;
public PeopleViewModel()
{
// initialize with sample data
_peopleList = getPeopleList();
}
// gets sample data
private List<Person> getPeopleList()
{
var result = new List<Person>();
for (int i = 0; i < 10; i++)
{
result.Add(new Person("person " + i, i));
}
return result;
}
public List<Person> PeopleList
{
get { return _peopleList; }
}
public Person SelectedPerson
{
get { return _selectedPerson; }
set
{
if (_selectedPerson == value) return;
_selectedPerson = value;
// required so that View know about changes
OnPropertyChanged("SelectedPerson");
}
}
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
// WPF will listen on this event for changes
public event PropertyChangedEventHandler PropertyChanged;
}
XAML for ComboBox:
<ComboBox Name="cmbEnum" Width="150" ItemsSource="{Binding Path=PeopleList}" SelectedValue="{Binding Path=SelectedPerson}" SelectedValuePath="" DisplayMemberPath="Name" ></ComboBox>
And in code behind I can do:
public Window2()
{
InitializeComponent();
vm = new PeopleViewModel();
// we are listening on changes of ViewModel, not ComboBox
vm.PropertyChanged += new PropertyChangedEventHandler(vm_PropertyChanged);
this.DataContext = vm;
}
void vm_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "SelectedPerson")
{
MessageBox.Show(vm.SelectedPerson.Age.ToString());
}
}
// button1_Click should be probably replaced by Command
private void button1_Click(object sender, RoutedEventArgs e)
{
// sample showing that GUI is updated when ViewModel changes
vm.SelectedPerson = vm.PeopleList[2];
}
Hope this helps, I'm quite new to WPF, I'd like to hear any feedback if this is the right way to use MVVM, I think it's quite elegant since you only deal with the ViewModel and Model in code, and the View can be replaced.
I Found that the best way of implementing this is define some external class for all lookups that i use in grid and embedd them in the template as a static resource
We ended up having classes with static properties for each of of our combo box lists:
(you can't make the class itself static otherwise XAML won't be able to open it, but you won't get compile errors)
For example:
public class ZoneList
{
private static readonly IList<Zone> _Items = new List<Zone>();
public static IList<Zone> Items
{
get { return _Items; }
}
}
and then in XAML:
<UserControl.Resources>
<ResourceDictionary>
<ObjectDataProvider x:Key="myZoneList" ObjectType="{x:Type StaticLists:ZoneList}"/>
</ResourceDictionary>
</UserControl.Resources>
<ComboBox ItemsSource="{Binding Path=Items, Source={StaticResource myZoneList}}"></ComboBox>