I have a combobox and two buttons on the same page. The buttons are "Previous" and "Next". Ideally I'd like to use the buttons to select combobox items accordingly. Also, I'd like "Next" and "Previous" buttons to be disabled when end or beginning of the list is reached. I don't think I would have a problem with doing this in code-behind but I wonder if this is possible through xaml/binding/mvvm. Thanks.
I would suggest the following approach:
Regular Combobox
Buttons (Previous and Next) with Enabled property binded to ComboBox SelectedIndex using a Converter
You can then set the new Index using regular button Events or thorugh Commands
Yes it's perfectly possible. Firstly, I would strongly recommend that you use an MVVM framework, secondly using a framework such as Caliburn.Micro, you can easily invoke verbs (methods) on your view model from Buttons in your view.
So, for example, you would have a <Button x:Name="Next">Next</Button> in your view, and a corresponding public void Next() method in your view model. Caliburn.Micro will take care of the convention based binding from the Button click event to the view model's method with the same name.
In your Next method, you would increment the current page. This would also be a public property on your view model, which implements INotifyPropertyChanged. An MVVM framework such as Caliburn.Micro provides a method which will invoke the PropertyChanged event using a lambda expression, rather than a magic property name string.
Then, in your view, you would bind the ComboBox ItemsSource to a collection of possible page numbers, and the SelectedItem to the current page public property.
As it sounds like you're doing paging, you might also want to consider the PagedList library (UI agnostic) and corresponding NuGet package.
Quick and Dirty Implementation .
cs :
public partial class MainWindow : Window , INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private List<string> _items;
public List<string> Items
{
get
{
if (_items == null)
_items = new List<string> { "1", "2", "3", "4", "5" };
return _items;
}
}
private string _selectedItem;
public string SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
if(PropertyChanged!= null)
PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
}
}
private ICommand _nextCommand;
public ICommand NextCommand
{
get
{
if (_nextCommand == null)
_nextCommand = new RelayCommand<string>(MoveToNext, CanMoveToNext);
return _nextCommand;
}
}
private ICommand _previousCommand;
public ICommand PreviousCommand
{
get
{
if (_previousCommand == null)
_previousCommand = new RelayCommand<string>(MoveToPrevious, CanMoveToPrevious);
return _previousCommand;
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
private void MoveToNext(object currentItem)
{
var currentIndex = _items.IndexOf((string)currentItem);
SelectedItem = _items.ElementAt(currentIndex + 1);
}
private bool CanMoveToNext(object currentItem)
{
return currentItem != null && _items.Last<string>() != (string)currentItem;
}
private void MoveToPrevious(object currentItem)
{
var currentIndex = _items.IndexOf((string)currentItem);
SelectedItem = _items.ElementAt(currentIndex - 1);
}
private bool CanMoveToPrevious(object currentItem)
{
return currentItem != null && _items.First<string>() != (string)currentItem;
}
}
RelayCommand
XAML :
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Content="Next" Command="{Binding NextCommand}" CommandParameter="{Binding ElementName=box, Path=SelectedItem}"/>
<Button Content="Prev" Grid.Column="1" Command="{Binding PreviousCommand}" CommandParameter="{Binding ElementName=box, Path=SelectedItem}"/>
<ComboBox x:Name="box"
Grid.Row="1" Grid.ColumnSpan="2"
ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedItem}"
/>
</Grid>
Related
I am developing an application in WPF in which I want to populate a ListBox on selecting a node of a TreeView. I have populated the TreeView using MVVM pattern. It actually contains the the drives (C:\, D:...) and their corresponding sub-folders. The sub-folders are the nodes. On selecting these nodes the respective files should be shown in the ListBox. I know the C# code to get all the files in a folders, I have also implemented the same. However, I am not getting any clue to map them, so that on selecting the nodes the files in them should get reflected in the ListBox.
Can any please help me in this regard? The application is being developed in MVVM pattern and I need in the same pattern itself.
First add a Files collection class to your folder class (used in the tree view)
public class FolderItem
{
// other class code
private ObservableCollection<File> _Files = null;
public ObservableCollection<File> Files
{
get
{
if (_Files == null) _Files = GetFiles();
return _Files;
}
set
{
_Files = value;
}
}
}
Then bind the listbox to the selected treeview item.
<ListBox ItemsSource="{Binding ElementName=myTreeView, Path=SelectedItem.Files}"/>
You might have quite a number of files and folders so I think I would be inclined to lazy load as much as I could.
That means the viewmodel doesn't need to go iterate through the entire hard drive initially but you need some way of acting when selecteditem changes.
You can't bind selecteditem to a viewmodel because it's read only.
I would therefore use a behaviour like:
Data binding to SelectedItem in a WPF Treeview
Bind a SelectedFolder using that.
In the setter of SelectedFolder go get the list of folders and files for that folder and fill the two collections. One is the child collection of that selecteditem - for it's folders.
The other is an observableCollection for the files to see in the listbox.
Make that a propfull and implement inotifyproprtychanged so when I set it to a new collection it notifies the ui.
Bind that collection to the itemssource of the listbox.
Basicly The MVVM pattern uses three Layers :
The Model : Basicly it contains the Model classes and the business logic to get and manipulate Data information.
The ViewModel : It acts as an intermediate layer between the Model and the Views, it is attached to the different views.
The views : The différent views of the app.
Here an example how to fill a Window with list of Drives and Files.
Class BindableBaseViewModel
namespace TalkRepeater.ViewModel
{
public class BindableBaseViewModel : DependencyObject,INotifyPropertyChanged
{
protected virtual void SetProperty<T>(ref T member, T val,[CallerMemberName] string propertyName = null)
{
if (object.Equals(member, val)) return;
member = val;
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
}
Class ViewModel
public class FoldersControlViewModel : BindableBaseViewModel
{
private ObservableCollection<Folders> _listFolders;
private ObservableCollection<Folders> _listFiles;
public FoldersControlViewModel()
{
FoldersBusinessObject vbo =new FoldersBusinessObject()
vbo.FillFolders();
ListFolders = FileBusinessObject.ListFolders;
}
public ObservableCollection<Folders> ListFolders
{
get
{
return _listFolders;
}
set
{
_listFolders = value;
OnPropertyChanged("ListFolders");
}
}
public ObservableCollection<Folders> ListFiles
{
get
{
return _listFiles;
}
set
{
_listFiles = value;
OnPropertyChanged("ListFiles");
}
}
Public void FillListFiles()
{
/*ListFiles= Cod to fill ListFiles*/
}
}
Class BusinessObject
public class FoldersBusinessObject
{
private ObservableCollection<Folders> _ListFolders;
public void FillFolders()
{
/* ListFolders= Code To fill the collection ListFolders */
}
public ObservableCollection<Folders> ListFolders
{
get
{
return _ListFolders;
}
set
{
_ListFolders = value;
}
}
}
Foldersview
<Window x:Class="Foldersview"
xmlns = "http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x = "http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc = "http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d = "http://schemas.microsoft.com/expression/blend/2008"
d:DesignHeight = "300" Width="1007" Height="606">
<Grid Margin="10" >
<Canvas x:Name="canvasFolders" Margin="-10,0,912,10">
<TreeView x:Name="TreevFolders" ItemsSource="{Binding Path=ListFolders, Mode=TwoWay}" Canvas.Top="5" Canvas.Left="17" Width="142" Height="561"
SelectedItemChanged="TreevFolders_SelectedItemChanged" >
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Path=ListFolders}">
<TextBlock Text="{Binding Path=FileName}">
</TextBlock>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Canvas>
<Canvas Margin="159,10,0,10">
<Listview x:Name="Listview1" ItemsSource="{Binding ListFiles, Mode=TwoWay}" >
</Listview>
</Canvas>
</Grid>
</Window>
Class Foldersview Code Behind
public partial class Foldersview : Window
{
private void TreevFolders_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
FoldersControlViewModel vmd = (FoldersControlViewModel)this.DataContext;
vmd.FillListFiles ();
}
}
Class Mainwindow
public class MainWindowViewModel : BindableBase
{
private FoldersControlViewModel FoldersviewModel;
public MainWindowViewModel()
{
FoldersviewModel = new FoldersControlViewModel();
Foldersview=new Foldersview();
Foldersview.Datacontext=FoldersviewModel;
}
}
Cordialy
I've defined a ListView where each item is displayed with a read-only (i.e. selection-only, no TextBox) ComboBox (as per the ItemTemplate).
There is a given list of possible items that can be chosen from each of these combo boxes. The caveat, however, is that no item may be selected in two combo boxes at a time. In order to ensure this, once an item has been selected in one of the combo boxes, it must be removed from all other combo boxes (obviously except for the one where it is selected), and once it gets deselected, it must be added to all other combo boxes again.
Oh, and one more thing: The order of the visible items must not change compared to the complete list.
My question is: How can I achieve this behavior?
I have tried the three possible solutions that came to my mind:
I have written a new helper control class that takes the complete list of existing items and a list of excluded (used) items via bindings to the outside world, as well as a property for a selected item. I could include that control class in the ItemTemplate; the combo box within that template would then bind its ItemsSource property to an ItemsProvider property of the helper class that was supposed to tie together the list of existing items, the list of excluded items and the selected item and return a single enumeration of items for that particular combo box.
However, I somehow got lost in all the update notifications about list changes; I fear I'd have to individually react to all combinations of NotifyCollectionChangedAction of the two input lists, and with the prospect of having a dozen update methods, I thought this cannot be the right way.
I changed the list of existing items to a list that stores a boolean along with each item, so I could flag each item as hidden or not. This would relieve me of the necessity to have a list of excluded items while maintaining the order of items, thereby reducing the aforementioned complexity of combined change notifications.
Unfortunately, as the list itself doesn't change with that solution, I don't know how to have the dependency property infrastructure notify my ItemsSource property in my helper class.
I don't have to use the WPF with bindings way; I can do code-behind here, too. So, I tried iterating over all ListViewItems and retrieving the combo box for each of them to manually refresh the item lists. However, I couldn't find a good time to access the ListViewItems after their item template had been loaded. There seems to be no event for that situation, and ListView.ItemContainerGenerator is read-only, so even if ItemContainerGenerator were not a sealed class, I couldn't assign my own specialized ItemContainerGenerator that would create custom list view items where I could override OnApplyTemplate.
I would probably bind all ComboBoxes to different CollectionViews over the source collection which filter out the selected items of the other ComboBoxes. You also need to Refresh the views if the selections of the combo-boxes change.
If you bind the Lists to different Lists in the ViewModel, and bind the selected item to trigger a method that changes those Lists, then you could get your result. Similar to the below.
Xaml of MainWindow.xaml:
<Window x:Class="ComboBox.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180" />
<ColumnDefinition Width="180" />
<ColumnDefinition Width="180" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="26" />
</Grid.RowDefinitions>
<ComboBox Name="cboOne" Grid.Column="0" Grid.Row="0" ItemsSource="{Binding CboOneList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding CboOneValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBox>
<ComboBox Name="cboTwo" Grid.Column="1" Grid.Row="0" ItemsSource="{Binding CboTwoList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding CboTwoValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBox>
<ComboBox Name="cboThree" Grid.Column="2" Grid.Row="0" ItemsSource="{Binding CboThreeList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" SelectedValue="{Binding CboThreeValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></ComboBox>
</Grid>
</Window>
Code-behind for MainWindow.xaml:
using System.Windows;
using System.Windows.Controls;
namespace ComboBox {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
this.DataContext = new ComboBoxViewModel();
}
private void cboOne_SelectionChanged(object sender, SelectionChangedEventArgs e) {
}
}
}
ViewModel:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace ComboBox {
class ComboBoxViewModel : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
List<string> master = new List<string>() { "A", "B", "C", "D", "E", "F" };
#region C'tor
public ComboBoxViewModel() {
RetrieveLists();
}
#endregion
#region Methods
protected void OnPropertyChanged(String propertyName) {
PropertyChangedEventHandler handler = this.PropertyChanged;
if(null != handler) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public void RetrieveLists() {
List<string> tempOne = (from a in master
where !a.Equals(CboTwoValue) && !a.Equals(CboThreeValue)
select a).ToList();
CboOneList = tempOne;
List<string> tempTwo = (from a in master
where !a.Equals(CboOneValue) && !a.Equals(CboThreeValue)
select a).ToList();
CboTwoList = tempTwo;
List<string> tempThree = (from a in master
where !a.Equals(CboTwoValue) && !a.Equals(CboOneValue)
select a).ToList();
CboThreeList = tempThree;
}
#endregion
#region Properties
private string cboOneValue = string.Empty;
public string CboOneValue {
get {
return cboOneValue;
}
set {
if(!value.Equals(cboOneValue)) {
cboOneValue = value;
RetrieveLists();
OnPropertyChanged("CboOneValue");
}
}
}
private string cboTwoValue = string.Empty;
public string CboTwoValue {
get {
return cboTwoValue;
}
set {
if(!value.Equals(cboTwoValue)) {
cboTwoValue = value;
RetrieveLists();
OnPropertyChanged("CboTwoValue");
}
}
}
private string cboThreeValue = string.Empty;
public string CboThreeValue {
get {
return cboThreeValue;
}
set {
if(!value.Equals(cboThreeValue)) {
cboThreeValue = value;
RetrieveLists();
OnPropertyChanged("CboThreeValue");
}
}
}
private List<string> cboOneList = new List<string>();
public List<string> CboOneList {
get {
return cboOneList;
}
set {
cboOneList = value;
OnPropertyChanged("CboOneList");
}
}
private List<string> cboTwoList = new List<string>();
public List<string> CboTwoList {
get {
return cboTwoList;
}
set {
cboTwoList = value;
OnPropertyChanged("CboTwoList");
}
}
private List<string> cboThreeList = new List<string>();
public List<string> CboThreeList {
get {
return cboThreeList;
}
set {
cboThreeList = value;
OnPropertyChanged("CboThreeList");
}
}
#endregion
}
}
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.
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).
I have a screen with a ListBox of items. The item template contains an expander control with some of the data in the header and some of the data in the content part of the expander.
The data template for the ListBox ItemTemplate is similar to this:
<DataTemplate x:Key="MyTypeTemplate" DataType="{x:Type MyType}">
<Expander DataContext="{Binding}">
<Expander.Header>
<Canvas>
<TextBox Text="{Binding MyProperty}"/>
</Canvas>
</Expander.Header>
<Canvas>
<TextBox Text={Binding MyDetailedProperty}"/>
</Canvas>
</Expander>
</DataTemplate>
Whenever these properties change, either 'MyProperty' or 'MyDetailedProperty' changes, the expander control collapsed. I believe that is has something to do with the Expander item getting recreated when the data changes.
As an additional data item, the list being bound to the listbox implements IBindingList as it comes from a library created for .NET 2.0. I cannot recreate the list using ObservableCollection due to time constraints
I ended up wrapping my model objects in a view object that adds an IsExpandable property that I could bind to the Expanded IsExpanded property and then exposed the data.
This is not a general purpose solution but it solves my immediate problem. The possible issues that I see that I haven't explored are whether the PropertyChanged and ListChanged event attaches cause memory leak issues with my UI objects, but in my situation each object should only be created once.
Also, events beyond Add and Remove in the collection change are not supported, but in my case I'm not firing anything else so it is safe for me to ignore them.
public class ExpandableItem<T> : INotifyPropertyChanged
where T: INotifyPropertyChanged
{
private bool m_isExpanded;
private readonly T m_data;
public ExpandableItem(T data)
{
m_data = data;
m_data.PropertyChanged +=
delegate
{
PropertyChanged(this, new PropertyChangedEventArgs("Data"));
};
}
public bool IsExpanded
{
get { return m_isExpanded; }
set
{
if (value != m_isExpanded)
{
m_isExpanded = value;
PropertyChanged(this, new PropertyChangedEventArgs("IsExpanded"));
}
}
}
public T Data
{
get
{
return m_data;
}
}
public event PropertyChangedEventHandler PropertyChanged = delegate { };
}
public class ExpandableList<TObject,TList> :
ObservableCollection<ExpandableItem<TObject>>
where TList : ObservableCollection<TObject>
where TObject : INotifyPropertyChanged
{
readonly TList m_list;
public ExpandableList(TList list)
: base(list.Select(obj=>new ExpandableItem<TObject>(obj)))
{
list.CollectionChanged += OnListChanged;
m_list = list;
}
public TList Data { get { return m_list; } }
private void OnListChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
Insert(e.NewStartingIndex, e.NewItems[0]);
}
if (e.Action == NotifyCollectionChangedAction.Remove)
{
RemoveAt(e.OldStartingIndex);
}
}
}