I was experimenting with WPF and encountered some filtering behavior that I did not expect.
I created a simple Window control with a ListView and a DataGrid, which display information about US presidents, such as name, party, and numerical order.
The application instantiates an ObservableCollection with several presidents. In Main, a view is created from the ObservableCollection, and filtering and sorting is applied. The ListView is bound to this view, and the DataGrid is bound to the original ObservableCollection.
I expected the ListView to display the filtered result and the DataGrid to display all the items in the list. However, the DataGrid displays the filtered result as well. Does anyone have an explanation for this?
public partial class MainWindow : Window
{
ICollectionView presidentView;
ObservableCollection<President> presidents = new ObservableCollection<President>
{
new President{Name = "Barack Obama", Party="Democratic", Order=44},
new President {Name = "George W Bush", Party="Republican", Order=43},
new President{Name = "Bill Clinton", Party="Democratic", Order=42},
new President {Name="George Bush", Party="Republican", Order=41},
new President{Name="Ronald Reagan", Party="Republican", Order=40},
new President{Name="Jimmy Carter", Party="Democratic", Order=39},
new President{Name="Gerald Ford", Party="Republican", Order=38},
new President{Name="Richard Nixon", Party="Republican", Order=37},
new President{Name="Lyndon Johnson", Party="Democratic", Order=36}
};
public MainWindow()
{
InitializeComponent();
presidentView = CollectionViewSource.GetDefaultView(presidents);
presidentView.SortDescriptions.Add(new SortDescription("Order", ListSortDirection.Ascending));
Predicate<object> isRepublican = (x) =>
{
President p = x as President;
return p.Party == "Republican";
};
presidentView.Filter = isRepublican;
list.ItemsSource = presidentView;
grid.ItemsSource = presidents;
}
}
public class President
{
public int Order { set; get; }
public string Name { set; get; }
public string Party { set; get; }
}
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow" Height="350" Width="727.416">
<Grid>
<ListView HorizontalAlignment="Left" Height="260" Margin="10,10,0,0" Name="list" VerticalAlignment="Top" Width="197">
<ListView.ItemTemplate>
<ItemContainerTemplate>
<TextBlock Text="{Binding Path=Name}">
</TextBlock>
</ItemContainerTemplate>
</ListView.ItemTemplate>
</ListView>
<DataGrid Name="grid" HorizontalAlignment="Left" Margin="224,13,0,0" VerticalAlignment="Top" Height="257" Width="487"/>
</Grid>
</Window>
CollectionViewSource.GetDefaultView(object) returns the same ICollectionView instance for the given source -- which will be used for any ItemsControl (the DataGrid) when displaying the source collection (presidents).
You can get around this by creating a new instance of ICollectionView to be used by each control that you want independent from other controls (typically, a different one for each different filter).
Update your presidentView to be instantiated like this:
public MainWindow()
{
InitializeComponent();
presidentView = new CollectionViewSource { Source= presidents }.View;
presidentView.SortDescriptions.Add(new SortDescription("Order", ListSortDirection.Ascending));
Predicate<object> isRepublican = (x) =>
{
President p = x as President;
return p.Party == "Republican";
};
presidentView.Filter = isRepublican;
list.ItemsSource = presidentView;
grid.ItemsSource = presidents;
}
Related
I want to bind my Datatemplate to 2 Datasources, one datasource that will actually define what is in the ListBox and other that will determine how many ListBoxes are there and what Items in the Listbox are selected\checked.
I have following XAML
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="TokenListTemplate">
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="chkToken" IsChecked="{Binding Path=IsSelected, Mode=TwoWay}">
<TextBlock Text="{Binding Path=Text}" />
</CheckBox>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="ItemTemplate">
<Border BorderThickness="1">
<StackPanel Margin="3">
<TextBlock Text="{Binding Path=Header}"/>
<ListBox ItemTemplate="{StaticResource TokenListTemplate}"
ItemsSource="{Binding Path=Tokens}" >
</ListBox>
</StackPanel>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ListBox ItemTemplate="{StaticResource ItemTemplate}"
ItemsSource="{Binding}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
</Grid>
And this is the codebehind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
ObservableCollection<DataEntity> _actualObjects;
List<Token> tokens1 = new List<Token>()
{
new Token("1"),
new Token("2"),
new Token("3"),
new Token("4")
};
List<Token> tokens2 = new List<Token>()
{
new Token("11"),
new Token("21"),
new Token("31")
};
_actualObjects = new ObservableCollection<DataEntity>()
{
new DataEntity(tokens1, "A", "1,2,3", 1),
new DataEntity(tokens1, "B", "2,3", 1),
new DataEntity(tokens2, "C", "21,31", 2)
};
DataContext = _actualObjects;
}
class DataEntity
{
public DataEntity(List<Token> tokens, string header, string tokenString, int entityTypeId)
{
Tokens = tokens;
Header = header;
TokenString = tokenString;
EntityTypeId = entityTypeId;
}
public List<Token> Tokens { get; set; }
public String Header { get; set; }
public String TokenString { get; set; }
public int EntityTypeId { get; set; }
}
public class Token
{
public bool IsSelected { get; set; }
public string Text { get; set; }
public Token(string text)
{
this.IsSelected = false;
this.Text = text;
}
}
}
It produces this
I don't want to inject token1 or token2 List into DataEntity object so in other words I want DataEntity constructor to be
public DataEntity(string header, string tokenString, int entityTypeId)
Listbox DataTemplate should select
tokens1 List as datasource for its LisBoxItems if
Dataentity.EntityTypeId = 1
tokens2 List as datasource for its LisBoxItemsif
DataEntity.EntityTypeId = 2
Also TokenString in DataEntity should be bound to items in the Listbox i.e. if Listbox shows 1 2 3 4
and DataEntity for this listbox has its TokenString value set to "1,2,3" then 1 2 3 should be checked in the listbox
I would recommend to create a ViewModel as a layer between your model and the view. In the ViewModel you can arrange the data to fit to the used controls without changing your model.
So the ViewModel could for example split the tokenString of the DataEntity into a list of tokens.
Just Google for MVVM (Model-View-ViewModel) for examples and furter explanations or look here on SO (like MVVM: Tutorial from start to finish?).
You're not thinking about this correctly. You need to create one class (some may call a view model) with the responsibility of providing all of the data that the view (or UI) will need. Therefore, you will need to have one property which holds a collection of type DataEntity (if I understand you correctly) to 'define what is in the outer ListBox' as you say.
Then you need a DataTemplate to describe what should be displayed for each item in the ListBox - your 'ItemTemplate' template. This DataTemplate should have another ListBox inside in which to display your Token objects. Your DataEntity should have something like this property in it:
public List<Token> Tokens
{
get
{
if (EntityTypeId == 1) return tokens1;
else if (EntityTypeId == 2) return tokens2;
}
}
You will then need another DataTemplate for your Token objects - your 'TokenListTemplate' template, but without the StackPanel... the inner ListBox replaces that, eg. if there are two Token objects in one DataEntity object, then that object would show two Checkboxes... you have correctly bound the IsChecked property to the Token.IsSelected property.
This may be complicated, but it is entirely possible. Just start with the first layer and get your DataEntity objects displayed in the outer ListBox using your 'ItemTemplate' template. Once that bit is ok, move on to the inner ListBox. Good luck.
I'm new to Prism, but I have successfully built several WPF/Mvvm-Light applications. I'm using ViewModel-first instaciation for each View/ViewModel pair. The views are all loaded and deactivated when the application opens. Views are activated as a result of catching an aggregate event aimed at them. This is the first view I've tried to bind to data in a ViewModel. The view displays as expected, except that my listbox is never populated. Only the outline of the listbox is visible. If I change the background color of the listbox, the color of the empty listbox is changed. The ViewModel property has eight rows but none of them are visible. I am able to display hardcoded items in the list box. I know that the view model is loading into the view as the data context, since another textblock is able to bind to a ViewModel property It must be something broken in my listbox xaml. Here is some xaml to review:
<UserControl
x:Class="DxStudioSelect.View.DxStudioFindView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
>
<UserControl.Resources>
<DataTemplate x:Key="DxStudioListTemplate">
<TextBlock Text="{Binding Path=FriendlyForkName}"/>
</DataTemplate>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<ListBox
Grid.Column="0"
ItemsSource="{Binding DatabaseInstanceList}"
ItemTemplate="{StaticResource DxStudioListTemplate}"
/>
<TextBlock Text="{Binding Path=PageName}" Grid.Column="1" FontSize="32" Foreground="Green" TextAlignment="Right"/>
</Grid>
</UserControl>
Here is the code-behind:
public partial class DxStudioFindView : UserControl, IDxStudioFindView {
public DxStudioFindView() {
InitializeComponent();
}
public IViewModel ViewModel {
get { return (IDxStudioFindViewModel)DataContext; }
set { DataContext = value; }
}
}
Here is the ViewModel:
private readonly IEventAggregator _eventAggregator;
private readonly IUnityContainer _unityContainer;
private readonly IRegionManager _regionManager;
private readonly string _dxStudioDatabaseName;
private readonly HeaderUpdatePayload _headerUpdatePayload = new HeaderUpdatePayload("DxStudio", "Select DxStudio Instance");
public DxStudioFindViewModel(IUnityContainer unityContainer, IRegionManager regionManager, IEventAggregator eventAggregator, IDxStudioFindView view)
: base(view) {
_unityContainer = unityContainer;
_regionManager = regionManager;
_eventAggregator = eventAggregator;
View.ViewModel = this;
if(IsInDesignMode) {
//Design-time, so show fake data
DesignTimeDataLoad();
} else {
//Run-time, so do the real stuff
DesignTimeDataLoad();
_dxStudioDatabaseName = LiteralString.DxStudioDatabaseNameTest;
_eventAggregator.GetEvent<ViewChangeRequestEvent>().Subscribe(DxStudioInstanceChangeRequest, ThreadOption.UIThread, false, target => target.TargetView == LiteralString.DxStudioFind);
}
}
public string PageName { get; set; }
//public string PageName { get { return "Find DxStudio Instance"; } }
private ObservableCollection<IDxStudioInstanceDto> _dxStudioInstanceList = null;
public ObservableCollection<IDxStudioInstanceDto> DxStudioInstanceList {
get { return _dxStudioInstanceList; }
set {
_dxStudioInstanceList = value;
OnPropertyChanged("DxStudioInstanceList");
}
}
private void DxStudioInstanceChangeRequest(ViewChangeRequestPayload payload) {
var region = _regionManager.Regions[RegionNames.Content];
region.Activate(View);
_eventAggregator.GetEvent<ViewChangedHeaderEvent>().Publish(_headerUpdatePayload);
var footerUpdatePayload = new FooterUpdatePayload(FooterDisplayMode.DxStudioSelect, _dxStudioDatabaseName, payload.TargetBackDatabase, payload.TargetBack, string.Empty, LiteralString.ToolboxStart);
_eventAggregator.GetEvent<ViewChangedFooterEvent>().Publish(footerUpdatePayload);
}
private void DesignTimeDataLoad() {
PageName = "Find DxStudio Instance";
DxStudioInstanceList = new ObservableCollection<IDxStudioInstanceDto>() {
new DxStudioInstanceDto("Instance1"),
new DxStudioInstanceDto("Instance2"),
new DxStudioInstanceDto("Instance3"),
new DxStudioInstanceDto("Instance4"),
new DxStudioInstanceDto("Instance5"),
new DxStudioInstanceDto("Instance6"),
new DxStudioInstanceDto("Instance7"),
new DxStudioInstanceDto("Instance8"),
};
}
And here is the data transfer object:
public class DxStudioInstanceDto : IDxStudioInstanceDto {
public string FriendlyForkName { get; private set; }
public DxStudioInstanceDto(string friendlyForkName) { FriendlyForkName = friendlyForkName; }
}
Since I'm completely out of ideas, any suggestion would be helpful.
Thanks
Your list is binding to ItemsSource="{Binding DatabaseInstanceList}" but your view model has the property DxStudioInstanceList.
I'm displaying a list of custom objects (here: Customer) dynamicly, each in its own tab using TabControl, ItemsSource and DataTemplate:
MainWindows.xaml.cs:
using System.Collections.Generic;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace testTabControl
{
public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int NumberOfContracts { get; set; }
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
//create all
var customers = new List<Customer>
{
new Customer {FirstName = "Jim", LastName = "Smith", NumberOfContracts = 23},
new Customer {FirstName = "Jane", LastName = "Smiths", NumberOfContracts = 42},
new Customer {FirstName = "John", LastName = "Tester", NumberOfContracts = 32}
};
//show
myTabControl.ItemsSource = customers;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
Trace.WriteLine("myTabControl.SelectedContent is " + myTabControl.SelectedContent.GetType());
Trace.WriteLine("myTabControl.SelectedItem is " + myTabControl.SelectedItem.GetType());
// do something with content of the selected tab:
///myTabControl.SelectedContent.Foreground = new SolidColorBrush(Color.FromRgb(255, 0, 0));
}
}
}
MainWindow.xaml:
<Window x:Class="testTabControl.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:testTabControl="clr-namespace:testTabControl"
Title="MainWindow" Height="350" Width="525"
SizeChanged="OnSizeChanged"
>
<Window.Resources>
<DataTemplate x:Key="myContent" DataType="{x:Type testTabControl:Customer}">
<TextBlock x:Name="myContentRoot">
This is the content for
<TextBlock Text="{Binding FirstName}"/> <TextBlock Text="{Binding LastName}"/>
</TextBlock>
</DataTemplate>
<DataTemplate x:Key="myHeader" DataType="{x:Type testTabControl:Customer}">
<TextBlock Text="{Binding LastName}"/>
</DataTemplate>
</Window.Resources>
<TabControl
x:Name="myTabControl"
SelectedIndex="0"
ItemTemplate="{StaticResource ResourceKey=myHeader}"
ContentTemplate="{StaticResource ResourceKey=myContent}"
/>
</Window>
In the method OnSizeChanged I expected myTabControl.SelectedContent to return a TextBlock generated from DataTemplate. But an instance of Customer is returned! Same as by myTabControl.SelectedItem.
The only way I found to get the generated content is described here: http://msdn.microsoft.com/en-us/library/bb613579.aspx
Does anybody know any other solution without visual tree walk?
Your TabControl contains objects of type Customer. Templates can be used to tell WPF how to draw each Customer object, however it doesn't change the fact that the items in the TabControl are still Customer objects.
If you want to access a templated UI object for the item, you can either walk the VisualTree or use the ItemContainerGenerator to get the container holding the SelectedItem
For example,
var selectedItem = myTabControl.SelectedItem;
var selectedItemContainer =
myTabControl.ItemContainerGenerator.ContainerFromItem(selectedItem);
The reason your SelectedContent is returning a Customer object is this:
<DataTemplate x:Key="myContent" DataType="{x:Type testTabControl:Customer}">
It is returning exactly what you ask for. All the DataTemplate does is describe how to display the Customer object which is the content, nothing else. When you ask for content, it returns the object that the DataTemplate is displaying.
One way that you could do this house a property on your Customer object which replicates the output you want.
public string OutputString
{
get
{
return string.Format("This is the content for {0} {1}", this.FirstName, this.LastName);
}
}
And then do something like
Trace.WriteLine("myTabControl.SelectedContent is " + myTabControl.SelectedContent.OutputString);
Or you might could create a collection of strings that followed the format above, and make those the content. Then your SelectedContent would be a simple string by default.
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.
I am currently experimenting with WPF.
One thing, I wanted to do was a master to detail selection over multiple comboboxes.
I have a ViewModel with GroupItems that i use as ItemSource for the first combobox. These GroupItems have a Property called Childs, which includes a List of items that belong to this group.
I can't find a way to bind the comboBox1.SelectedItem.Childs as Itemsource for the second comboBox.
Right now I only got to
ItemsSource="{Binding ElementName=comboBox1, Path=SelectedItem}"
But I don't get the Property of the SelectedItem. How can this be done? Or is this not the WPF way to this?
Is there any good website to learn how to select different elements? Eplaining Path, XPath, Source and everything?
Thanks for any help.
Your binding above isn't attempting to bind to Childs, only SelectedItem.
Try something like this:
Window1.xaml
<Window x:Class="WpfApplication5.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 x:Name="_groups" ItemsSource="{Binding Groups}" DisplayMemberPath="Name"/>
<ComboBox ItemsSource="{Binding SelectedItem.Items, ElementName=_groups}"/>
</StackPanel>
</Window>
Window1.xaml.cs
using System.Windows;
namespace WpfApplication5 {
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
var model = new ViewModel();
var g1 = new Group { Name = "Group1" };
g1._items.Add("G1C1");
g1._items.Add("G1C2");
g1._items.Add("G1C3");
model._groups.Add(g1);
var g2 = new Group { Name = "Group2" };
g2._items.Add("G2C1");
g2._items.Add("G2C2");
g2._items.Add("G2C3");
model._groups.Add(g2);
var g3 = new Group { Name = "Group3" };
g3._items.Add("G3C1");
g3._items.Add("G3C2");
g3._items.Add("G3C3");
model._groups.Add(g3);
DataContext = model;
}
}
}
ViewModel.cs
using System;
using System.Collections.Generic;
namespace WpfApplication5
{
public class Group {
internal List<String> _items = new List<string>();
public IEnumerable<String> Items {
get { return _items; }
}
public String Name { get; set; }
}
public class ViewModel
{
internal List<Group> _groups = new List<Group>();
public IEnumerable<Group> Groups
{
get { return _groups; }
}
}
}