TreeView Selected Item Change - wpf

Having an issue with selection of my treeview.
The idea is that the user clicks the item and data is added under the node that they selected
public MainWindow()
{
InitializeComponent();
this.UserControl1.TreeView1.SelectedItemChanged += new RoutedPropertyChangedEventHandler<Object>(InterfaceTreeViewComputers_SelectionChange);
}
void InterfaceTreeViewComputers_SelectionChange(Object sender, RoutedPropertyChangedEventArgs<object> e)
{
var MyTreeView = MainWindow.UserControl1.Treeview1.Items.SourceCollection;
var TheSource = sender as TreeView;
var TheProperty = e.Source;
var ThePropertyAsTreeView = TheProperty as TreeView
TreeViewItem item = e.OriginalSource as TreeViewItem; //Equals Null
var Attempt2 = ThePropertyAsTreeView.SelectedItem //Equals Null
var Attempt3 = TheSource.SelectedItem as TreeViewItem //Equals Null
var Attempt4 = TheSource.SelectedItem //Equals onbject(String)
}
It seems that the selected item is a textblock and i cant seem to find a way to get it as a treeview item to add nodes under it.
Yes i am pretty new to this type of programming.
Thank you for any help you may provide.

Try this to get selected item:
TreeViewItem item = e.NewValue as TreeViewItem;
Or this to get previously selected item:
TreeViewItem item = e.OldValue as TreeViewItem;
e.Source and e.OriginalSource refer to the TreeView not the TreeViewItem selected. You can use breakpoint then see those properties value and type in Visual Studio's watch. That what I was doing before posting this.

I believe you are approaching this the wrong way. Most of your problems will disappear if you use DataBinding to populate your TreeView. Take into account the following class
namespace StackOverflow._20716616
{
public class Model
{
public Model()
{ Items = new ObservableCollection<Model>(); }
public Model(string text)
: this()
{ Text = text; }
public string Text { get; set; }
public ObservableCollection<Model> Items { get; set; }
}
}
Using this class as the model of an MVVM pattern, I can create a hierarchical object graph which I can bind to the TreeView using ItemsSource and a HierarchicalDataTemplate like
<TreeView ItemsSource="{Binding Path=Items}" SelectedItemChanged="TreeView_OnSelectedItemChanged">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate DataType="{x:Type this:Model}" ItemsSource="{Binding Path=Items}">
<TextBlock Text="{Binding Path=Text}" />
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
The DataContext of the window was set up on the root node of the xaml
DataContext="{Binding RelativeSource={RelativeSource Self}}"
In the code behind, I populate the root items of the TreeView
public MainWindow()
{
Items = new ObservableCollection<Model>() { new Model("one"), new Model("two"), new Model("three") };
InitializeComponent();
}
public ObservableCollection<Model> Items { get; set; }
Then, in the SelectedItemChanged, I can just add the children to the object graph
private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
var model = e.NewValue as Model;
if (!model.Items.Any())
{
new List<Model>() { new Model("one"), new Model("two"), new Model("three") }
.ForEach(model.Items.Add);
}
// expand the selected item.
var item = ((TreeView)sender).ItemContainerGenerator.ContainerFromItem(model) as TreeViewItem;
item.IsExpanded = true;
}
Note, this is poor man's MVVM where I have used the Windows code behind as the ViewModel. I have also left out null checks and exception catching for the sake of brevity.
I hope this helps

Your right, i am so sorry. http://www.dotnetperls.com/ have given me heaps of insite on this language.
Im too used to programming in things like Visual Basic and Delphi. ive realize now that my MainWindow needs to perform all the pysical actions to my front end user interface and the classes in the other namespaces do not do this.
I have realize that you call a class to perform a specific operation. the class then either returns the information or stores the information in a public property. in which the MainWindow can utilize this and perform the Action required. This i didnt realizeand was trying to manipulate my User Interface from other classes.
I have since restructed my project and the information that was provided here has helped me with my events.
now i have UI coded in XAML
Main Window (Performin events and updating the UI)
Other Classes (Getting data, or designing controls) so the Main Window can add the control or the data to the UI.
I reel like a nub now.

Related

Display Hierarchical Data in TreeView

I am simply trying to display hierarchical Data in a TreeView, but I just can't figure out how to make it display more than the First two Levels. (And i have read almost evry TreeView post, maybe the problem is my (miss)understatement of the Bindings in this case)
I have simplyfied my Datastructure for this test:
public class Node
{
public List<Node> Children { get; set; }
public Node Parent { get; set; }
public string Expression { get; set; }
}
Xaml currently looks like this: (Please notice that I have changed it several times now, but this here is the original state i have come up with: )
<Window x:Class="Klammern_Test.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Klammern_Test"
Title="MainWindow" Height="439" Width="402">
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Node}">
<TreeViewItem ItemsSource="{Binding Children}" Header="{Binding Expression}"/>
</HierarchicalDataTemplate>
</Window.Resources>
<Grid>
<TreeView ItemsSource="{Binding Root}" Margin="12,41,12,12" Name="treeView" />
</Grid>
</Window>
And this is how I am trying to Populate my Treeview:
public partial class MainWindow : Window
{
public Node Root { get; set; }
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void StartButton_Click(object sender, RoutedEventArgs e)
{
Parser = new StringParser();
Root = Parser.Parse(Tbx_Eingabe.Text);
treeView.Items.Add(PopulateTreeView(Root));
}
private TreeViewItem PopulateTreeView(Node node)
{
TreeViewItem treeViewItem = new TreeViewItem();
treeViewItem.IsExpanded = true;
treeViewItem.Header = node.Expression;
foreach (Node child in node.Children)
{
treeViewItem.Items.Add(new TreeViewItem() { Header = child.Expression });
if (child.Children.Count > 0)
{
PopulateTreeView(child);
}
}
return treeViewItem;
}
}
What am I missing?
EDIT:
After trying around with almulo's hints, I found this with the Snoop-tool, but I can't tell what it means at all, I have found no other red line and no entry to the Binding Errors column at all.
Using a TreeViewItem inside a HierarchicalDataTemplate which is what your TreeView's TreeViewItems will use to create themselves is... confusing.
Inside your HierarchicalDataTemplate you should just add the controls you want the item "header" to have. In this case, I guess it should be a TextBlock since you just wanna show some text.
Then use the HierarchicalDataTemplate.ItemSource property to bind the children of your node.
<Window.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Node}"
ItemsSource="{Binding Children}">
<TextBlock Text="{Binding Expression}" />
</HierarchicalDataTemplate>
</Window.Resources>
Also, in you code-behind, you shouldn't manipulate the TreeView.Items or the TreeViewItem.Items directly, since you are already using Bindings and the ItemsSource properties.
Instead, remove the PopulateTreeView method and let your Root property work as items source for the TreeView. But in order for this to work, you'll have to notify the view when the Root property changes its value.
To do so, implement the INotifyPropertyChanged interface and fire the PropertyChanged event every time Root changes.
EDIT: The ItemsControl property ItemsSource expects a collection (more specifically, an IEnumerable), so Root needs to be one. Even if it has a single item, like this:
public class MainWindow : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// ...
private void StartButton_Click(object sender, RoutedEventArgs e)
{
Parser = new StringParser();
Root = new Node[] { Parser.Parse(Tbx_Eingabe.Text) };
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Root"));
}
// ...
}

Setting WPF datacontext for a specific control

I'm developing a WPF application and I'm struggling a little bit to understand some of the details of DataContext as it applies to binding. My application uses a business object which is defined like this:
public class MyBusinessObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
{
PropertyChanged(this, e);
}
}
// enumerations for some properties
public enum MyEnumValues
{
[Description("New York")]
NewYork,
[Description("Chicago")]
Chicago,
[Description("Los Angeles")]
LosAngeles
}
// an example property
private string _myPropertyName;
public string MyPropertyName
{
get { return _myPropertyName; }
set
{
if (_myPropertyName == value)
{
return;
}
_myPropertyName = value;
OnPropertyChanged(new PropertyChangedEventArgs("MyPropertyName"));
}
}
// another example property
private MyEnumValues _myEnumPropertyName;
public MyEnumValues MyEnumPropertyName
{
get { return _myEnumPropertyName; }
set
{
if (_myEnumPropertyName== value)
{
return;
}
_myEnumPropertyName= value;
OnPropertyChanged(new PropertyChangedEventArgs("MyEnumPropertyName"));
}
}
// example list property of type Widget
public List<Widget> MyWidgets { get; set; }
// constructor
public MyBusinessObject()
{
// initialize list of widgets
MyWidgets = new List<Widget>();
// add 10 widgets to the list
for (int i = 1; i <= 10; i++)
{
MyWidgets.Add(new Widget());
}
// set default settings
this.MyPropertyName = string.empty;
}
}
As you can see, I have some properties that are declared in this class one of which is a list of Widgets. The Widget class itself also implements INotifyPropertyChanged and exposes about 30 properties.
My UI has a combobox which is bound to my list of Widgets like this:
MyBusinessObject myBusinessObject = new MyBusinessObject();
public MainWindow()
{
InitializeComponent();
this.DataContext = myBusinessObject;
selectedWidgetComboBox.ItemsSource = myBusinessObject.MyWidgets;
selectedWidgetComboBox.DisplayMemberPath = "WidgetName";
selectedWidgetComboBox.SelectedValuePath = "WidgetName";
}
The majority of the controls on my UI are used to display the properties of a Widget. When my user selects a Widget from the combobox, I want these controls to display the properties for the selected Widget. I'm currently achieving this behavior by updating my window's DataContext in the SelectionChanged event handler of my combobox like this:
private void selectedWidgetComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
this.DataContext = selectedWidgetComboBox.SelectedItem;
}
This allows me to bind my controls to the appropriate Widget property like this:
<TextBox Text="{Binding WidgetColor}"></TextBox>
However, not all of the controls in my UI are used to display Widget properties. Some of the controls need to display the properties from MyBusinessObject (for example: MyPropertyName defined above). In this scenario, I can't simply say:
<TextBox Text="{Binding MyPropertyName}"></TextBox>
...because the DataContext of the window is pointing to the selected Widget instead of MyBusinessObject. Can anyone tell me how I set the DataContext for a specific control (in XAML) to reference the fact that MyPropertyName is a property of MyBusinessObject? Thank you!
Instead of changing the DataContext of your window, you should add a property to your MyBusinessObject class like this one:
private Widget _selectedWidget;
public Widget SelectedWidget
{
get { return _selectedWidget; }
set
{
if (_selectedWidget == value)
{
return;
}
_selectedWidget = value;
OnPropertyChanged(new PropertyChangedEventArgs("SelectedWidget"));
}
}
Then bind SelectedWidget to the SelectedItem property of your combobox. Anywhere that you need to use the widget's properties you can do this:
<TextBox Text="{Binding Path=SelectedWidget.WidgetColor}"></TextBox>
try
<TextBox Text="{Binding MyBusinessObject.MyPropertyName}"></TextBox>
this works if MyBusinessObject is the datacontext of the textbox and MyPropertyName is a property of MyBusinessObject
Also, Here is a good article to clarify binding
hope this helps
EDIT 1:
use a relative binding like this:
text="{Binding DataContext.MyPropertyName, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TypeOfControl}}}"
So the relatve binding allows you to look up the visual tree to another UI element and use its datacontext. I would consider wrapping your window's contents in a grid. and wet your windows datacontext to the businessobject and the grids datacontext to the widget. That way you can always use the parent window's datacontext through the realtive source binding.
so use the following if your window's datacontext is your business object
text="{Binding DataContext.MyPropertyName, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"

Simple WPF data binding

I want to separate my user interface from my code, so I (obviously) landed at bindings. As a test, I've written the following XAML:
<Window x:Class="BindingTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="Auto" Width="200">
<StackPanel>
<TextBox Text="{Binding Item}"/>
<Button Content="Add" Click="AddNew"/>
<ListBox Height="100" ItemsSource="{Binding Items}"/>
</StackPanel>
</Window>
The C# looks like this:
namespace BindingTest
{
public partial class MainWindow : Window
{
public string Item { get; set; }
public ObservableCollection<string> Items { get; set; }
public MainWindow()
{
InitializeComponent();
Items = new ObservableCollection<string>();
}
private void AddNew(object sender, RoutedEventArgs e)
{
Items.Add(Item);
}
}
}
What I want to happen is that the text entered into the textbox is added to the listbox's itemssource. However, this doesn't happen...
Two things you need two do -
Set - DataContext = this; in your constructor.
You'd be better off if you would change your properties to dependency properties instead. You could do that easily with the "propdp" snippet in visual studio.
Data binding is performed against the current data context. However, you have not set the data context for your window. Often you will set the data context to a view model but in your case you simply want to use the window class for that.
You should add the following line to the constructor:
DataContext = this;
Change your code to this:
public partial class MainWindow : Window
{
public string Item { get; set; }
public ObservableCollection<string> Items { get; set; }
public MainWindow()
{
InitializeComponent();
Items = new ObservableCollection<string>();
DataContext = this;
}
private void AddNew(object sender, RoutedEventArgs e)
{
Items.Add(Item);
}
}
}
You do need to set your DataContext - works for me.
Two things:
You should set the correct data context for your window. Otherwise the binding will not find your properties.
You should initialize your Items collection before the InitializeComponent() call as inside it the ListBox tries to evaluate the expression and get NULL as the binding souce. And since you are not implementing INotifyPropertyChanged and the property is not a DependencyProperty the ListBox will never reevaluate the binding thus it will never get the instance of your Items collection.
So, the code should be as follows:
public MainWindow()
{
Items = new ObservableCollection<string>();
DataContext = this;
InitializeComponent();
}
Try this
hope this will work. But this is not hte right approach. You need to set the DataContext to the Object whose properties u guna use for binding. you must follow MVVM Architecture.

WPF data binding - what am I missing?

I am trying to grasp the concepts of WPF data binding through a simple example, but it seems I haven't quite gotten the point of all of it.
The example is one of cascading dropdowns; the XAML is as follows:
<Window x:Class="CascadingDropDown.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="496" Width="949" Loaded="Window_Loaded">
<Grid>
<ComboBox Name="comboBox1" ItemsSource="{Binding}" DisplayMemberPath="Key" SelectionChanged="comboBox1_SelectionChanged" />
<ComboBox Name="comboBox2" ItemsSource="{Binding}" DisplayMemberPath="Name" />
</Grid>
</Window>
This is the code of the form:
public partial class MainWindow : Window
{
private ObservableCollection<ItemA> m_lstItemAContext = new ObservableCollection<ItemA>();
private ObservableCollection<ItemB> m_lstItemBContext = new ObservableCollection<ItemB>();
private IEnumerable<ItemB> m_lstAllItemB = null;
public MainWindow()
{
InitializeComponent();
this.comboBox1.DataContext = m_lstItemAContext;
this.comboBox2.DataContext = m_lstItemBContext;
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var lstItemA = new List<ItemA>() { new ItemA("aaa"), new ItemA("bbb"), new ItemA("ccc") };
var lstItemB = new List<ItemB>() { new ItemB("aaa", "a11"), new ItemB("aaa", "a22"), new ItemB("bbb", "b11"), new ItemB("bbb", "b22") };
initPicklists(lstItemA, lstItemB);
}
private void initPicklists(IEnumerable<ItemA> lstItemA, IEnumerable<ItemB> lstItemB)
{
this.m_lstAllItemB = lstItemB;
this.m_lstItemAContext.Clear();
lstItemA.ToList().ForEach(a => this.m_lstItemAContext.Add(a));
}
#region Control event handlers
private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
ComboBox ddlSender = (ComboBox)sender;
ItemA itemaSelected = (ItemA)ddlSender.SelectedItem;
var lstNewItemB = this.m_lstAllItemB.Where(b => b.KeyA == itemaSelected.Key);
this.m_lstItemBContext.Clear();
lstNewItemB.ToList().ForEach(b => this.m_lstItemBContext.Add(b));
}
private void comboBox2_?(object sender, ?EventArgs e)
{
// disable ComboBox if empty
}
#endregion Control event handlers
}
And these are my data classes:
class ItemA
{
public string Key { get; set; }
public ItemA(string sKey)
{
this.Key = sKey;
}
}
class ItemB
{
public string KeyA { get; set; }
public string Name { get; set; }
public ItemB(string sKeyA, string sName)
{
this.KeyA = sKeyA;
this.Name = sName;
}
}
So whenever an item is selected in comboBox1, the appropriate items are supposed to show up in comboBox2. This is working with the current code, though I'm not sure whether my way of re-populating the respective ObservableCollection is ideal.
What I haven't been able to achieve is actually reacting to changes in the underlying collection of comboBox2, for example to deactivate the control when the list is empty (i.e. when "ccc" is selected in comboBox1).
Of course, I can use an event handler on the CollectionChanged event of the ObservableCollection, and that would work in this example, but in a more complex scenario, where the ComboBox' DataContext might change to a completely different object (and possibly back), that would mean a two-fold dependency - I would always have to not only switch the DataContext, but also the event handlers back and forth. This doesn't seem right to me, but I am probably simply on an entirely wrong track about this.
Basically, what I am looking for is an event firing on the control rather than the underlying list; not the ObservableCollection announcing "my contents have changed", but the ComboBox telling me "something happenend to my items".
What do I need to do, or where do I have to correct my perception of the whole concept ?
Here is the cleaner (perhaps not the much optimized) way to acheive this, keeping your business model untouched, and using ViewModel and XAML only when possible :
View Model :
public class WindowViewModel : INotifyPropertyChanged
{
private ItemA selectedItem;
private readonly ObservableCollection<ItemA> itemsA = new ObservableCollection<ItemA>();
private readonly ObservableCollection<ItemB> itemsB = new ObservableCollection<ItemB>();
private readonly List<ItemB> internalItemsBList = new List<ItemB>();
public WindowViewModel()
{
itemsA = new ObservableCollection<ItemA> { new ItemA("aaa"), new ItemA("bbb"), new ItemA("ccc") };
InvokePropertyChanged(new PropertyChangedEventArgs("ItemsA"));
internalItemsBList = new List<ItemB> { new ItemB("aaa", "a11"), new ItemB("aaa", "a22"), new ItemB("bbb", "b11"), new ItemB("bbb", "b22") };
}
public ObservableCollection<ItemA> ItemsA
{
get { return itemsA; }
}
public ItemA SelectedItem
{
get { return selectedItem; }
set
{
selectedItem = value;
ItemsB.Clear();
var tmp = internalItemsBList.Where(b => b.KeyA == selectedItem.Key);
foreach (var itemB in tmp)
{
ItemsB.Add(itemB);
}
InvokePropertyChanged(new PropertyChangedEventArgs("SelectedItem"));
}
}
public ObservableCollection<ItemB> ItemsB
{
get { return itemsB; }
}
public event PropertyChangedEventHandler PropertyChanged;
public void InvokePropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
}
Code Behind :
public partial class Window1
{
public Window1()
{
InitializeComponent();
DataContext = new WindowViewModel();
}
}
and XAML :
<StackPanel>
<ComboBox Name="comboBox1" ItemsSource="{Binding ItemsA}" DisplayMemberPath="Key" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
<ComboBox Name="comboBox2" ItemsSource="{Binding ItemsB}" DisplayMemberPath="Name">
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}">
<Setter Property="IsEnabled" Value="true"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ItemsB.Count}" Value="0">
<Setter Property="IsEnabled" Value="false"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
</StackPanel>
copying-pasting this should work.
Few random thoughts :
1) in WPF, try to always use MVVM pattern and never put code in code-behind files for event handlers. For user actions (like button clicks) use the Commands pattern. For other user actions for which commands are not available, think as much as you can in a "binding-way" : you can do a lot since you can intercept event from the view in VM properties setters (in your example I use the SelectedItem property setter).
2) Use XAML as much as you can. WPF framework provides a very powerful binding and triggers system (in your example, the enabling of combobox don't needs any line of C#).
3) ObservableCollection are made to be exposed by the view model to the view via binding. They are also meant to be used in conjunction with their CollectionChanged event that you can handle in the view model. Take benefit of that (in your example, I play with Observable collection in the VM, where this playing should happen, and any changes in the collection gets reflected in the view via DataBinding).
Hopes this will help !
Basically, what I am looking for is an event firing on the control rather than the underlying list; not the ObservableCollection announcing "my contents have changed", but the ComboBox telling me "something happenend to my items"
if you wanna use MVVM pattern then i would say NO. not the control should give the information, but your viewmodel should.
taking an ObservableCollection is a good step at first. in your specail case i would consider to create just one list with ItemA and i would add a new List property of type ItemB to ItemA.
class ItemA
{
public string Key { get; set; }
public ItemA(string sKey)
{
this.Key = sKey;
}
public IEnumerable<ItemB> ListItemsB { get; set;}
}
i assume ItemA is the parent?
class ItemB
{
public string Name { get; set; }
public ItemB(string sName)
{
this.Name = sName;
}
}
you have a collection of ItemA and each ItemA has its own list of depending ItemB.
<ComboBox x:Name="cbo_itemA" ItemsSource="{Binding ListItemA}" DisplayMemberPath="Key"/>
<ComboBox ItemsSource="{Binding ElementName=cbo_itemA, Path=SelectedItem.ListItemsB}"
DisplayMemberPath="Name" />
Do you need the Keys collection? If not i'd suggest creating it dynamically from the items by grouping via CollectionView:
private ObservableCollection<object> _Items = new ObservableCollection<object>()
{
new { Key = "a", Name = "Item 1" },
new { Key = "a", Name = "Item 2" },
new { Key = "b", Name = "Item 3" },
new { Key = "c", Name = "Item 4" },
};
public ObservableCollection<object> Items { get { return _Items; } }
<StackPanel>
<StackPanel.Resources>
<CollectionViewSource x:Key="ItemsSource" Source="{Binding Items}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Key"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</StackPanel.Resources>
<StackPanel.Children>
<ComboBox Name="keyCb" ItemsSource="{Binding Source={StaticResource ItemsSource}, Path=Groups}" DisplayMemberPath="Name"/>
<ComboBox ItemsSource="{Binding ElementName=keyCb, Path=SelectedItem.Items}" DisplayMemberPath="Name"/>
</StackPanel.Children>
</StackPanel>
The first ComboBox shows the keys which are generated by grouping by the Key-property, the second binds to the selected item's subitems in the first ComboBox, showing the Name of the item.
Also see the CollectionViewGroup reference, in the fist CB i use the Name in the second the Items.
Of course you can create these key-groups manually as well by nesting items in a key-object.

WPF ComboBox resets selected item when item-source changes

Consider the following XAML:
<ComboBox Name="CompanyComboBox"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Path=GlobalData.Companies}"
SelectedValuePath="Id"
SelectedValue="{Binding Customer.CompanyId, ValidatesOnDataErrors=True}"
DisplayMemberPath="Name" />
GlobalData.Companies is a collection (IEnumerable<Company>) of companies; this collection can be reloaded on background (it is downloaded from a webservice). When this happens, ComboBox correctly reloads items via binding. However as a side-effect, it also resets the selected item!
I have used Reflector to inspect combo-box sources and apparently this is intended behavior.
Is there any "nice" way how to get around this? What I want to achieve, is that if the user selects "Company A" and reloads list of companies afterwards, then "Company A" stays selected (assuming it is in the new list).
Please try with the following code.
Enable the following property to the combo box
IsSynchronizedWithCurrentItem="True"
Maybe you can use ObservableCollection<Company> instead of your IEnumerable<Company>? Then, on background change you would only Add / Remove items that are new / absent in the new list, selected item should stay, unless it was removed by the change.
You can update your observable collection in a separate thread with a small hack-around.
hmm, I don't know if it is a "nice" way, but if you can access the selected item before the reload occurs, you can save it (or its key or something), and select it programatically again after the reload is done.
quick mockup:
var selectedItem = myCombo.SelectedItem;
DoReload();
myCombo.SelectedItem = selectedItem;
But I assume you mean another way than this manual work around?
Hope this helps anyway...
UPDATE
Ok I see, from a background thread.
Are you using an ICollectionView to bind your combobox too? If so, you can use the CurrentItem property to keep a reference. I made a quick mockup, and this is working on my setup. this assumes you have a reference to your UI:
XAML
<Grid VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ComboBox ItemsSource="{Binding Items}" IsSynchronizedWithCurrentItem="True" Grid.Column="0" Grid.Row="0" DisplayMemberPath="Name"/>
<Button Command="{Binding UpdateCommand}" Grid.Column="1" Grid.Row="0">Update</Button>
</Grid>
View/ViewModel
public partial class Window1 : Window {
public Window1() {
InitializeComponent();
this.DataContext = new ViewModel(this);
}
}
public class ViewModel
{
private readonly Window1 window;
private ObservableCollection<Item> items;
private ICollectionView view;
public ViewModel(Window1 window) {
this.window = window;
items = new ObservableCollection<Item>
{
new Item("qwerty"),
new Item("hello"),
new Item("world"),
};
view = CollectionViewSource.GetDefaultView(items);
}
public ObservableCollection<Item> Items { get { return items; } }
public ICommand UpdateCommand {
get { return new RelayCommand(DoUpdate); }
}
public Item SelectedItem { get; set; }
private void DoUpdate(object obj) {
var act = new Func<List<Item>>(DoUpdateAsync);
act.BeginInvoke(CallBack, act);
}
private List<Item> DoUpdateAsync() {
return new List<Item> {
new Item("hello"),
new Item("world"),
new Item("qwerty"),
};
}
private void CallBack(IAsyncResult result) {
try {
var act = (Func<List<Item>>)result.AsyncState;
var list = act.EndInvoke(result);
window.Dispatcher.Invoke(new Action<List<Item>>(delegate(List<Item> lst) {
var current = lst.Single(i => i.Name == ((Item)view.CurrentItem).Name);
Items.Clear();
lst.ForEach(Items.Add);
view.MoveCurrentTo(current);
}), list);
} catch(Exception exc){ Debug.WriteLine(exc); }
}
}
public class Item {
public Item(string name) {
Name = name;
}
public string Name { get; set; }
}
You will need to do some handling in case the selected item is no longer in the list.
The IsSynchronizedWithCurrentItem property is important here, else it won't work!
Also, the way the reference to the main window is made should be by a DI-framework.
As Yacoder pointed out this has to do with object equality. As long as you bind SelectedValue instead of SelectedItem you can define the ItemsSource as an anonymous type collection. Then this problem will not occur (and it is also faster if you need to read the values from a database).

Resources