I have scenario in Combobox, We want to change the current SelectedItem of Combobox when certain value is selected.
For Instance: We have designation Combobox having values: CEO, Manager, Dev, QA..
When CEO is selected we would like to change it to Manager value.
SelectedValue is bound to property in ViewModel.
Aha! I thought it was a silly question and could be answered without spending much effort :P. But, it thought me a lesson that WPF is not working the way what I think as always.
Here is the sample working solution.
MainWindow.xaml
<Grid>
<ComboBox Width="100" Height="50" ItemsSource="{Binding ComboList}" SelectedValue="{Binding Selected, Mode=TwoWay, IsAsync=True}"/>
</Grid>
MainWindow.xaml.cs
public MainWindow()
{
InitializeComponent();
ObservableCollection<string> lst = new ObservableCollection<string>();
lst.Add("CEO");
lst.Add("Tester");
lst.Add("president");
lst.Add("Developer");
lst.Add("Manager");
MainWindowViewModel vm = new MainWindowViewModel() { ComboList = lst, Selected = "Employee" };
this.DataContext = vm;
}
MainWindowViewModel.cs
public class MainWindowViewModel : INotifiPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
public ObservableCollection<string> ComboList { get; set; }
private string selected;
public string Selected
{
get { return selected; }
set
{
selected = value == "CEO" ? "Manager" : value;
OnPropertyChanged("Selected");
}
}
}
The Key is IsAsync=True which did the trick here. Thanks to Martin Harris for his answer
Related
I have simplified this problem down as much as I can. Basically I am overriding the "null" value of a combobox. So that if the item selected is deleted, it reverts back to "(null)". Unfortunately the behaviour of this is wrong, I hit delete, the ObservableCollection item is removed, thus the property binding is updated and it returns the "(null)" item as expected. But the combobox appearance shows blank. Yet the value its bound to is correct... this problem can be reproduced with the code below.
To reproduce this problem you select an item, and hit remove. Notice at this point the following line is called (when you remove the selected item). So its a good place to breakpoint.
if (m_Selected == null)
{
return Items[0]; //items 0 is ItemNull
}
Also notice that I have attmpted to fix it by Forcing a property update on the DisplayMemberPath. This did not work.
MainWindow.xaml
<Window x:Class="WPFCodeDump.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">
<StackPanel>
<ComboBox ItemsSource="{Binding Items}" SelectedItem="{Binding Selected, Mode=TwoWay}" DisplayMemberPath="Name"></ComboBox>
<Button Click="ButtonBase_OnClick">Remove Selected</Button>
</StackPanel>
</Window>
MainWindowViewModel.cs
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;
namespace WPFCodeDump
{
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
//Item class
public class Item : ViewModelBase
{
public Item(string name)
{
m_Name = name;
}
public string Name
{
get { return m_Name; }
}
private string m_Name;
public void ForcePropertyUpdate()
{
OnPropertyChanged("Name");
}
}
//Item class
public class ItemNull : Item
{
public ItemNull()
: base("(null)")
{
}
}
class MainWindowViewModel : ViewModelBase
{
public MainWindowViewModel()
{
m_Items.Add(new ItemNull());
for (int i = 0; i < 10; i++)
{
m_Items.Add(new Item("TestItem" + i));
}
Selected = null;
}
//Remove selected command
public void RemoveSelected()
{
Items.Remove(Selected);
}
//The item list
private ObservableCollection<Item> m_Items = new ObservableCollection<Item>();
public ObservableCollection<Item> Items
{
get { return m_Items; }
}
//Selected item
private Item m_Selected;
public Item Selected
{
get
{
if (m_Selected == null)
{
return Items[0]; //items 0 is ItemNull
}
return m_Selected;
}
set
{
m_Selected = value;
OnPropertyChanged();
if(m_Selected!=null) m_Selected.ForcePropertyUpdate();
}
}
}
}
MainWindow.xaml.cs
using System.Windows;
namespace WPFCodeDump
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
((MainWindowViewModel) DataContext).RemoveSelected();
}
}
}
Result:
A nice binding issue you found there. But as always, it's our fault, not theirs :)
The issue(s) is(are), using DisplayMemberPath with SelectedItem.
The DisplayMemberPath doesn't give a f*** about the changed SelectedItem.
What you have to do, to resolve this issue, are two things:
First, in the RemoveSelected method, set the Selected property to null (to force an update on the binding):
public void RemoveSelected()
{
Items.Remove(Selected);
Selected = null;
}
Then, in the XAML-definition, change the bound property:
<ComboBox ItemsSource="{Binding Items}"
SelectedValue="{Binding Selected, Mode=TwoWay}"
DisplayMemberPath="Name"/>
Binding the SelectedValue property will correctly update the displayed text in the ComboBox.
I have a ListView with items, that contain string field Name among others. Items in the ListView are sorted by this field:
SortDescription descr = new SortDescription("Name", ListSortDirection.Ascending);
list.Items.SortDescriptions.Add(descr);
I also have a TextBlock in which I want to display the Name of the first item in the sorted ListView. Items can be added, removed and edited at runtime, so I would like to use some kind of binding like the following one (doesn't work, just for example):
<TextBlock Text="{Binding ElementName=list, Path=Items[0].Name}"/>
1) How can I achieve the desired behavior using a binding?
2) If such binding can't be created, which is the most convenient way to succeed?
Any thoughts and hints would be appreciated.
UPDATE
Content of the main window:
<StackPanel>
<TextBlock Name="nameFirst" Text="{Binding ElementName=list, Path=Items[0].Name}"/>
<ListView Name="list" ItemsSource="{Binding ElementName=mainWnd, Path=List}" DisplayMemberPath="Name" Loaded="list_Loaded"/>
</StackPanel>
Code behind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public class Item : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public Item(string name)
{
Name = name;
}
private string _name;
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("Name");
}
}
}
public ObservableCollection<Item> _list;
public ObservableCollection<Item> List
{
get { return _list; }
set
{
_list = value;
OnPropertyChanged("List");
}
}
public MainWindow()
{
InitializeComponent();
List = new ObservableCollection<Item>();
List.Add(new Item("1"));
List.Add(new Item("2"));
List.Add(new Item("3"));
}
private void list_Loaded(object sender, RoutedEventArgs e)
{
SortDescription descr = new SortDescription("Name", ListSortDirection.Descending);
list.Items.SortDescriptions.Add(descr);
}
}
When the application is started, items in the ListView are sorted in the descending order: "3", "2", "1", but nameFirst TextBox still displays "1", though now it should display "3".
Actually, what you wrote should do the work. The problem is probably not in the xaml.
You said the items contains a field Name. WPF can only bind to public properties make sure to use them, and not public members. It's also possible you're seeing old values if you changed them after the window was opened. Make sure your items implement the INotifyPropertyChanged interface, and the properties invoke OnPropertyChanged.
If you're still having problems, please post more of your code, specifically your data items class and the place where you set the SortDescriptor.
Lastly, though it's not what you asked about. You can bind to the current item in an array by using the / operator. To use your code:
<TextBlock Text="{Binding ElementName=list, Path=Items/Name}"/>
You control the "current" item using CollectionViews and the IsSynchronizedWithCurrentItem property.
I have trouble when I try to select some Item in DataGrid programmatically. Without using MVVM pattern all is OK. Look at XAML:
<DataGrid
Name="_dataGrid"
AutoGenerateColumns="False"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}"/>
<DataGridTextColumn Binding="{Binding SecondName}"/>
</DataGrid.Columns>
</DataGrid>
Code behind:
public class GridItem
{
public String Name { get; set; }
public String SecondName { get; set; }
}
public partial class Window1 : Window
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(String propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
private GridItem _selectedItem;
public GridItem SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
RaisePropertyChanged("SelectedItem");
}
}
public Window1()
{
InitializeComponent();
DataContext = this;
_dataGrid.Loaded += DataGridLoaded;
Init1();
}
void DataGridLoaded(object sender, RoutedEventArgs e)
{
Int32 rowIndex = 2;
var selItem = _dataGrid.Items[rowIndex];
SelectedItem = (GridItem)selItem; <-------- Bad
//_dataGrid.SelectedItem = selItem; <-------- Good
}
private void Init1()
{
var source = new List<GridItem>();
source.Add(new GridItem
{
Name = "pavlik",
SecondName = "bobr"
});
source.Add(new GridItem
{
Name = "alex",
SecondName = "ugr"
});
source.Add(new GridItem
{
Name = "den",
SecondName = "ivanov"
});
source.Add(new GridItem
{
Name = "dima",
SecondName = "klim"
});
_dataGrid.ItemsSource = source;
}
}
So, when I select Item like that
_dataGrid.SelectedItem = selItem; // Good
Item is selected and highlighted properly.
But when I try to select and highlight Item via Model property, Item is not highlighted!
SelectedItem = (GridItem)selItem; // Bad
What is the reason? Any idea?
you need to inherit your window or any viewmodel class from INotifyPropertyChanged otherwise it will not Notifies clients that a property value has changed.
for your case it could be like
public partial class Window1 : INotifyPropertyChanged
{
// Class code goes here;
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(String propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
I do this a lot, DataGrid to ViewModel, and I never have to do
SelectedItem = (GridItem)selItem;
i.e cast the item.
As you have SelectedItem on the DataGrid XAML, it knows the type you are binding too.
However, I normally use ItemSource on the gridview as well.
e.g. ItemSource = "{Bind the collection of GridItem}"
so in ViewModel, I would have Observablecollection or List as a property.
My WPF knowledge is only like 2 weeks old so I could be totally be wrong, but I think the cast and the Itemsource is where you need to look.
I could give you an example if you like.
Cheers
I have question about DataContext changes and I build example for understand this aspect.
I have MainUserControl on MainWindow. MainUserControl consists of number of User Controls.
One of such User Controls is SubUserControl1.
<Window x:Class="WpfApplicationUcBindingQuestion.MainWindow">
<Grid>
.....
<uc:MainUserControl />
</Grid>
</Window>
<UserControl x:Class="WpfApplicationUcBindingQuestion.MainUserControl">
<Grid>
.....
<uc:SubUserControl1 x:Name="subUserControl1" />
</Grid>
</UserControl>
And in MainWindow I have object of class Info. Class Info consists of a few inner classes.
One of them is, lets say, SubInfo. Both Info and SubInfo classes inherits from INotifyPropertyChanged.
And this is the code of them:
public class Info : INotifyPropertyChanged
{
private SubInfo m_subInfo = new SubInfo();
public Info()
{
}
public SubInfo SubInfo
{
get
{
return m_subInfo;
}
set
{
m_subInfo = value;
OnPropertyChanged("SubInfo");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public class SubInfo: INotifyPropertyChanged
{
private string m_subString = "subStr";
public SubInfo()
{
}
public string SubString
{
get
{
return m_subString;
}
set
{
m_subString = value;
OnPropertyChanged("SubString");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I want to set DataContext for MainUserControl to be object of class Info
and for SubUserControl1 DataContext will be Info.SubInfo.
The following code describes this:
<UserControl x:Class="WpfApplicationUcBindingQuestion.SubUserControl1">
<Grid>
.....
<TextBox Text="{Binding Path=SubString}"/>
</Grid>
</UserControl>
public MainUserControl()
{
InitializeComponent();
MainWindow mainWnd = (MainWindow)Application.Current.MainWindow;
Info info = mainWnd.Info;
this.DataContext = info;
this.subUserControl1.DataContext = info.SubInfo;
}
When new subInfo arrived I update inner object subInfo inside info object:
(This is function of MainWindow)
private void OnUpdateData()
{
SubInfo arrivedSubInfo = new SubInfo();
arrivedSubInfo.SubString = "newString";
m_info.SubInfo = arrivedSubInfo;
}
I want to see that DataContext for subUserControl1 is also changed.
But it doesn't happened and TextBox inside SubUserControl1 is not updated
and doesn't show "newString".
(Note: If I write inside OnUpdateData() function the following:
m_info.SubInfo.SubString = arrivedSubInfo.SubString;
(copy field-field and not whole object) it works,
but I dont 'want copy 50 field...)
Where I'm wrong?
Your help will be really appreciated.
Your problem is the following:
In your constructor, when you do that:
this.DataContext = info;
this.subUserControl1.DataContext = info.SubInfo;
You will set the DataContext ONLY once. Which means it will never change unless you write subUserControl1.DataContext = someNewDataContext somewhere.
What you can do to solve this :
The "proper solution":
Use a binding. In your XAML, just write:
<uc:SubUserControl1 x:Name="subUserControl1" DataContext="{Binding
SubInfo, UpdateSourceTrigger=PropertyChanged}" />
This will work, assuming that your SubInfo property fires the OnPropertyChanged event when it is set.
The "ugly solution":
Explicitly set your UserControl's DataContext in code-behind when you need it. Again, I wouldn't advise that and you are better off applying the first solution!
I have a some XAML as follows (a simple Label and Button):
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Test="clr-namespace:WpfApplication2">
<StackPanel DataContext="{Binding Path=TestPerson}">
<Label Content="{Binding Path=Name}"></Label>
<Button Content="Button" Click="button1_Click" />
</StackPanel>
</Window>
in the code behind I have:
public partial class MainWindow : Window, INotifyPropertyChanged
{
private Person _person = new Person();
public Person TestPerson { get { return _person; } }
public MainWindow()
{
DataContext = this;
InitializeComponent();
}
private void button1_Click(object sender, RoutedEventArgs e)
{
_person.Name = "Bill";
//_person = new Person() { Name = "Bill" };
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("TestPerson"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
and the class Person is:
public class Person
{
string _name = "Bob";
public string Name
{
get { return _name; }
set { _name = value; }
}
}
As it is, firing the Propertychanged event does not cause the Label's contents to change to Bill.
I've found I am able to overcome this by either:
Assigning a new object to _person (as in the commented out line)
Removing the DataContext from the StackPanel and have Label bind to Path=TestPerson.Name
I don't understand why I have to actually assign a NEW object to _person for the Label to update, or use the FULL path in the binding.
Is there a way to update the Label without supplying the full path (relying on the DataContext), and without assigning a new object to _person?
You raise PropertyChanged for the Person instance TestPerson. However, TestPerson hasn't changed, it is the Name property of TestPerson that has changed and that is the property the Label is binding to.
Edit: To answer why your first two versions work
Assigning a new object to _person (as in the commented out line)
Here you are actually changing the value of TestPerson and because DataContext is inherited by the children, the Label gets a new DataContext as well so that's why the Binding is updated.
Removing the DataContext from the StackPanel and have Label bind to
Path=TestPerson.Name
This is something I've never seen. The same binding subscribes to PropertyChanged for both TestPerson and Name in Person so raising PropertyChanged for any of these properties will work.
If you want to overcome this without implementing INotifyPropertyChanged for Person, you can change set UpdateSourceTrigger to Explicit
<Label Name="label"
Content="{Binding Path=Name, UpdateSourceTrigger=Explicit}"/>
And update the Binding manually whenever Name changes
private void button1_Click(object sender, RoutedEventArgs e)
{
_person.Name = "Bill";
BindingExpression be = label.GetBindingExpression(Label.ContentProperty);
be.UpdateTarget();
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("TestPerson"));
}
}
Otherwise, just implement INotifyPropertyChanged for Person as well and it will work
public class Person : INotifyPropertyChanged
{
string _name = "Bob";
public string Name
{
get { return _name; }
set
{
_name = value;
OnPropertyChanged("Name");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
You need a little change in your XAML...
In your code behind, instead of setting DataContext as this, set it in XAML via Binding...
<StackPanel DataContext="{Binding RelativeSource={RelativeSource
AncestorType= {x:Type Window},
Mode=FindAncestor},
Path=TestPerson}">
<Label Content="{Binding Path=Name}"></Label>
<Button Content="Button" Click="button1_Click" />
</StackPanel>
Remove the
DataContext = this;
from your code behind.
Let me know if this helps.