I'm adding TreeViewItems manually in code behind and would like to use a DataTemplate to display them but can't figure out how to. I'm hoping to do something like this but the items are displayed as empty headers. What am I doing wrong?
XAML
<Window x:Class="TreeTest.WindowTree"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WindowTree" Height="300" Width="300">
<Grid>
<TreeView Name="_treeView">
<TreeView.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" />
<TextBlock Text="{Binding Path=Age}" />
</StackPanel>
</DataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
Behind code
using System.Windows;
using System.Windows.Controls;
namespace TreeTest
{
public partial class WindowTree : Window
{
public WindowTree()
{
InitializeComponent();
TreeViewItem itemBob = new TreeViewItem();
itemBob.DataContext = new Person() { Name = "Bob", Age = 34 };
TreeViewItem itemSally = new TreeViewItem();
itemSally.DataContext = new Person() { Name = "Sally", Age = 28 }; ;
TreeViewItem itemJoe = new TreeViewItem();
itemJoe.DataContext = new Person() { Name = "Joe", Age = 15 }; ;
itemSally.Items.Add(itemJoe);
_treeView.Items.Add(itemBob);
_treeView.Items.Add(itemSally);
}
}
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}
Your ItemTemplate is trying to render a "Name" and "Age" property in TextBlocks, but TreeViewItem doesn't have an "Age" property and you aren't setting its "Name".
Because you're using an ItemTemplate, there's no need to add TreeViewItems to the tree. Instead, add your Person instances directly:
_treeView.Items.Add(new Person { Name = "Sally", Age = 28});
The problem, of course, is that your underlying object ("Person") doesn't have any concept of hierarchy, so there's no simple way to add "Joe" to "Sally". There are a couple of more complex options:
You could try handling the TreeView.ItemContainerGenerator.StatusChanged event and wait for the "Sally" item to be generated, then get a handle to it and add Joe directly:
public Window1()
{
InitializeComponent();
var bob = new Person { Name = "Bob", Age = 34 };
var sally = new Person { Name = "Sally", Age = 28 };
_treeView.Items.Add(bob);
_treeView.Items.Add(sally);
_treeView.ItemContainerGenerator.StatusChanged += (sender, e) =>
{
if (_treeView.ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
return;
var sallyItem = _treeView.ItemContainerGenerator.ContainerFromItem(sally) as TreeViewItem;
sallyItem.Items.Add(new Person { Name = "Joe", Age = 15 });
};
}
Or, a better solution, you could introduce the hierarchy concept into your "Person" object and use a HierarchicalDataTemplate to define the TreeView hierarchy:
XAML:
<Window x:Class="TreeTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WindowTree" Height="300" Width="300">
<Grid>
<TreeView Name="_treeView">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Subordinates}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" />
<TextBlock Text="{Binding Path=Age}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Grid>
</Window>
CODE:
using System.Collections.Generic;
using System.Windows;
namespace TreeTest
{
/// <summary>
/// Interaction logic for Window1.xaml
/// </summary>
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
var bob = new Person { Name = "Bob", Age = 34 };
var sally = new Person { Name = "Sally", Age = 28 };
_treeView.Items.Add(bob);
_treeView.Items.Add(sally);
sally.Subordinates.Add(new Person { Name = "Joe", Age = 15 });
}
}
public class Person
{
public Person()
{
Subordinates = new List<Person>();
}
public string Name { get; set; }
public int Age { get; set; }
public List<Person> Subordinates { get; private set; }
}
}
This is a more "data-oriented" way to display your hierarchy and a better approach IMHO.
It will work if you pull your DataTemplate out of the TreeView and put it into Window.Resources. Like this:
<Window.Resources>
<DataTemplate DataType={x:type Person}>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=Name}" />
<TextBlock Text="{Binding Path=Age}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
Don't forget to add the right namespace before Person.
Related
I am very new to WPF, and help appreciated:
Model:
Collection<Presenter>,
Presenter has a Collection<Presentation>,
Presentation has a TeachingSession property (which contains a DateTime? property)
I am trying to have a treeview display:
presenter name
[combobox of available Dates]
At the moment, each presenter name in the treeview is displaying correctly, and the first parent item expanded displays the combobox with the correctly selected date. However, comboboxes displaying at any one time are all 'in sync' - that is changing the value in a combobox (or expanding a different treeview item) changes the value for all comboboxes which can be seen, so they all display the same date.
<TreeView Name="PresenterTreeView" ItemsSource="{Binding}">
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Presenter}"
ItemsSource="{Binding Path=Presentations}">
<TextBlock Text="{Binding Path=FullName}" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Presentation}">
<ComboBox SelectedItem="{Binding Path=TeachingSession, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="SessionDate"
ItemsSource="{Binding Source={StaticResource availableDatesViewSource}}" >
</ComboBox>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
I have compliled your example and I cant get the comboboxes to sync like you are describing.
Perhaps you will be able to see what I done different and it might be the fix, Or maybe I am just wrong and am missing somthing from your question?
This is the code I used:
Xaml:
<Window x:Class="WpfApplication8.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="340" Width="480" Name="UI" xmlns:local="clr-namespace:WpfApplication8">
<TreeView Name="PresenterTreeView" DataContext="{Binding ElementName=UI}" ItemsSource="{Binding Path=Presenters}" >
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type local:Presenter}" ItemsSource="{Binding Path=Presentations}">
<TextBlock Text="{Binding FullName}" />
</HierarchicalDataTemplate>
<HierarchicalDataTemplate DataType="{x:Type local:Presentation}">
<ComboBox SelectedItem="{Binding TeachingSession.SessionDate}" ItemsSource="{Binding ElementName=UI, Path=Dates}" />
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>
</Window>
Code:
public partial class MainWindow : Window
{
private ObservableCollection<Presenter> _myProperty = new ObservableCollection<Presenter>();
private ObservableCollection<DateTime?> _myDates = new ObservableCollection<DateTime?>();
public MainWindow()
{
InitializeComponent();
DateTime time1 = DateTime.Now;
DateTime time2 = DateTime.Now.AddDays(1);
Dates.Add(time1);
Dates.Add(time2);
for (int i = 0; i < 20; i++)
{
Dates.Add(DateTime.Now.AddDays(i));
}
TeachingSession teach = new TeachingSession { SessionDate = time1 };
Presentation pres = new Presentation { TeachingSession = teach };
Presenter presenter = new Presenter { FullName = "Presenter1" };
presenter.Presentations = new ObservableCollection<Presentation>();
presenter.Presentations.Add(pres);
TeachingSession teach1 = new TeachingSession { SessionDate = time2 };
Presentation pres1 = new Presentation { TeachingSession = teach1 };
Presenter presenter1 = new Presenter { FullName = "Presenter1" };
presenter1.Presentations = new ObservableCollection<Presentation>();
presenter1.Presentations.Add(pres1);
Presenters.Add(presenter);
Presenters.Add(presenter1);
}
public ObservableCollection<Presenter> Presenters
{
get { return _myProperty; }
set { _myProperty = value; }
}
public ObservableCollection<DateTime?> Dates
{
get { return _myDates; }
set { _myDates = value; }
}
}
public class Presenter
{
public string FullName { get; set; }
public ObservableCollection<Presentation> Presentations { get; set; }
}
public class Presentation
{
public TeachingSession TeachingSession { get; set; }
}
public class TeachingSession
{
public DateTime? SessionDate { get; set; }
}
Result:
Because of sa_ddam213's answer, i was able to track the problem down to the default synchronisation with the CollectionViewSource used to bindItemsSource
The combobox needed the attribute:
IsSynchronizedWithCurrentItem="False"
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've just stuck in a problem to bind collection in ItemsControl with ItemTeplate that contains bounded ComboBox.
In my scenario I need to "generate" form that includes textbox and combobox for each item in collection and let user to update items. I could use DataGrid for that but I'd like to see all rows in edit mode, so I use ItemsControl with custom ItemTemplate.
It's ok to edit textboxes but when you try to change any ComboBox, all other ComboBoxes in other rows will change too.
Is it a bug or feature?
Thanks, Ondrej
Window.xaml
<Window x:Class="ComboInItemsControlSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="480" Width="640">
<Window.Resources>
<CollectionViewSource x:Key="cvsComboSource"
Source="{Binding Path=AvailableItemTypes}" />
<DataTemplate x:Key="ItemTemplate">
<Border BorderBrush="Black" BorderThickness="0.5" Margin="2">
<Grid Margin="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding Path=ItemValue}" />
<ComboBox Grid.Column="2"
SelectedValue="{Binding Path=ItemType}"
ItemsSource="{Binding Source={StaticResource cvsComboSource}}"
DisplayMemberPath="Name"
SelectedValuePath="Value" />
</Grid>
</Border>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding Path=SampleItems}"
ItemTemplate="{StaticResource ItemTemplate}"
Margin="10" />
</Grid>
Window.xaml.cs
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
public class ViewModel
{
public ViewModel()
{
SampleItems = new List<SampleItem> {
new SampleItem { ItemValue = "Value 1" },
new SampleItem { ItemValue = "Value 2" },
new SampleItem { ItemValue = "Value 3" }
};
AvailableItemTypes = new List<SampleItemType> {
new SampleItemType { Name = "Type 1", Value = 1 },
new SampleItemType { Name = "Type 2", Value = 2 },
new SampleItemType { Name = "Type 3", Value = 3 },
new SampleItemType { Name = "Type 4", Value = 4 }
};
}
public IList<SampleItem> SampleItems { get; private set; }
public IList<SampleItemType> AvailableItemTypes { get; private set; }
}
public class SampleItem : ObservableObject
{
private string _itemValue;
private int _itemType;
public string ItemValue
{
get { return _itemValue; }
set { _itemValue = value; RaisePropertyChanged("ItemValue"); }
}
public int ItemType
{
get { return _itemType; }
set { _itemType = value; RaisePropertyChanged("ItemType"); }
}
}
public class SampleItemType : ObservableObject
{
private string _name;
private int _value;
public string Name
{
get { return _name; }
set { _name = value; RaisePropertyChanged("Name"); }
}
public int Value
{
get { return _value; }
set { _value = value; RaisePropertyChanged("Value"); }
}
}
public abstract class ObservableObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName) {
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Picture
here you can see the result on picture
I believe it's because you're binding to a CollectionViewSource, which tracks the current item. Try binding directly to your list instead, which won't track the current item
<ComboBox Grid.Column="2"
SelectedValue="{Binding Path=ItemType}"
DisplayMemberPath="Name"
SelectedValuePath="Value"
ItemsSource="{Binding RelativeSource={
RelativeSource AncestorType={x:Type ItemsControl}},
Path=DataContext.AvailableItemTypes}" />
While you have a combobox in each row, it doesnt see these comboboxes as being seperate. i.e. They are all using the same collection, and the same selectedValue, so when a value changes in one box, it changes in all of them.
The best way to fix this is to add the SampleItemType collection as a property on your SampleItem model and to then bind the combo box to that property.
I have 2 list boxes (ListA and ListB) that display data from different entities (EntityA, EntityB). These entities are related - EntityA has a property of a collection of EntityB. I want to be able to use drag and drop behaviour in order to add items from ListB into the collection of the dropped item in ListA.
To clarify, I don't want to add ListItemB into the collection of the selected ListItemA, I want to add it into the collection of the list item that I drop it onto (the ListItemA that the mouse is over when I release).
Using ListBoxDragDropTarget, is it possible for a ListBoxItem to be the drop target, instead of the listbox itself?
Any suggestions as to a solution for this scenario?
It can be done by creating two ListBoxes, as you described, one bound to an ObservableCollection<EntityA> and one bound to an ObservableCollection<EntityB>. EntityA contains an ObservableCollection<EntityB> as a property. The ListBox items of EntityA are templated to display the child collection of EntityB's as a ListBox. The ListBoxDragDropTarget is specified in this ItemTemplate, rather than the parent. Here is some XAML to demonstrate:
<ListBox Name="listOfEntityA">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding EntityName}" />
<toolKit:ListBoxDragDropTarget AllowDrop="True" AllowedSourceEffects="All">
<ListBox ItemsSource="{Binding ChildEntityBs}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding EntityName}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</toolKit:ListBoxDragDropTarget>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<toolKit:ListBoxDragDropTarget AllowDrop="True" AllowedSourceEffects="All">
<ListBox Name="listOfEntityB" />
</toolKit:ListBoxDragDropTarget>
After a bit of work I think I have it:
<StackPanel Orientation="Horizontal">
<Controls:ListBoxDragDropTarget AllowDrop="True">
<ListBox x:Name="FromBox" Width="200" ItemsSource="{Binding IssueList}" DisplayMemberPath="Name"/>
</Controls:ListBoxDragDropTarget>
<Controls:ListBoxDragDropTarget AllowDrop="True" Drop="ToBoxDragDropTarget_Drop">
<ListBox x:Name="ToBox" Width="150" ItemsSource="{Binding ObjectiveList}" DisplayMemberPath="Name" Margin="80,0,0,0" />
</Controls:ListBoxDragDropTarget>
<TextBlock x:Name="UpdateText"/>
</StackPanel>
and the codebehind (which will now be refactored into my ViewModel):
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
IssueList = new ObservableCollection<Issue>
{
new Issue{ ID = 1, Name="One"},
new Issue{ ID = 2, Name="Two"},
new Issue{ ID = 3, Name="Three"},
new Issue{ ID = 4, Name="Four"},
new Issue{ ID = 5, Name="Five"},
};
ObjectiveList = new ObservableCollection<Objective>
{
new Objective {ID = 10, Name = "Ten"},
new Objective {ID = 11, Name = "Eleven"},
new Objective {ID = 12, Name = "Twelve"},
new Objective {ID = 13, Name = "Thirteen"},
new Objective {ID = 14, Name = "Fourteen"},
};
LayoutRoot.DataContext = this;
}
public ObservableCollection<Issue> IssueList { get; set; }
public ObservableCollection<Objective> ObjectiveList { get; set; }
private void ToBoxDragDropTarget_Drop(object sender, Microsoft.Windows.DragEventArgs e)
{
var droppedOnObjective = ((FrameworkElement)e.OriginalSource).DataContext as Objective;
var args = e.Data.GetData(typeof(ItemDragEventArgs)) as ItemDragEventArgs;
if (args != null)
{
var draggedItems = args.Data as SelectionCollection;
var draggedItem = draggedItems[0];
if (droppedOnObjective != null)
{
var draggedIssue = (Issue)draggedItem.Item;
if (!droppedOnObjective.Issues.Contains(draggedIssue))
{
droppedOnObjective.Issues.Add(draggedIssue);
UpdateText.Text = string.Format("Issue <{0}> added to Objective <{1}>", draggedIssue.Name, droppedOnObjective.Name);
}
else
{
UpdateText.Text = string.Format("Objective <{0}> already contains Issue <{1}>", droppedOnObjective.Name, draggedIssue.Name);
}
}
else
UpdateText.Text = "selections or dropOnObjective is null";
}
else
UpdateText.Text = "args null";
}
}
public class Issue
{
public int ID { get; set; }
public string Name { get; set; }
}
public class Objective
{
public int ID { get; set; }
public string Name { get; set; }
public List<Issue> Issues { get; set; }
public Objective()
{
Issues = new List<Issue>();
}
}
I have a class ShipmentsCollection that inherits ObservableCollection which contains shipment objects (entities). This is displayed in a listbox on my ShipmentsView UserControl. My intent is to allow a user to type into a textbox above the list and filter the list with items that contain that string, as well as filter based on several checkbox and radiobutton based options (Delivery status and orderby direction).
I have tried this several ways, but none seem very elegant or really functional. Things I have tried follows:
Put ShipmentsCollection into a CollectionViewSource and filtered via predicate. Could not figure out a good way to make the filter auto update based on user typing or option change.
Refactored as a Class that Inherits collectionViewSource and tried to declare directly in XAML but got the following error: "The specified named connection is either not found in the configuration, not intended to be used with the EntityClient provider, or not valid". Tried fixing but could not find solution that worked.
Refactored to inherit from CollectionView, implemented filter logic, in event handler in codebehind. Still trying to figure out how I can get the filter string to the event handler without naming the filtertext textbox control.
Anyone got some good ideas in regard to implementing this functionality in an MVVM design pattern. I expect to have at most 200 objects in the list, so it will not be an enormous filter operation.
Cory
Your first option would be the one I would suggest. To get the auto-filter to work based on typing, I'd do something like a SearchString property in my ViewModel, bind the textbox text to that, and set the UpdateSourceTrigger in the binding to PropertyChanged so it will call the SearchString PropertyChanged event every time a key is typed instead of waiting until the box loses focus.
XAML:
<TextBox Text="{Binding SearchString, UpdateSourceTrigger=PropertyChanged}" />
ViewModel: With the above property set to PropertyChanged, the "Set" method gets called anytime a key is typed instead of just when the textbox loses focus.
private string _searchString;
public string SearchString
{
get { return _searchString; }
set
{
if (_searchString != value)
{
_searchString = value;
OnPropertyChanged("SearchString");
}
}
}
I know this question is closed and old. but for someone like me searching for dynamic filtering, can refer to the following link
https://github.com/lokeshlal/WPFDynamicFilters
the above example creates filters for each entity based on the attribute defined on property of entity model.
As an example:
Define an attribute for filters
public class FilterAttribute : Attribute
{
public FilterAttribute() { }
public string FilterLabel { get; set; }
public object FilterValue { get; set; }
public string FilterKey { get; set; }
public Type FilterDataType { get; set; }
public bool IsDropDown { get; set; }
public string DropDownList { get; set; }
public List<object> ObjectDropDownList { get; set; }
}
Apply the above attribute in model properties
public class GridModel
{
[Filter(FilterLabel = "Id",
FilterKey = "Id",
IsDropDown = false,
FilterDataType = typeof(int))]
public int Id { get; set; }
[Filter(FilterLabel = "Name",
FilterKey = "Name",
IsDropDown = false,
FilterDataType = typeof(string))]
public string Name { get; set; }
[Filter(FilterLabel = "Country",
FilterKey = "Country",
IsDropDown = true,
FilterDataType = typeof(int),
DropDownList = "Country")]
public string Country { get; set; }
[Filter(FilterLabel = "Address",
FilterKey = "Address",
IsDropDown = false,
FilterDataType = typeof(string))]
public string Address { get; set; }
}
Define the model that will bind to the drop down type
public class Country
{
public int Id { get; set; } // id will be used for value
public string Name { get; set; } // Name will be used for display value
}
ViewModel of actual View
public class FilterViewModel
{
public ICommand CheckFiltersCommand { get; set; }
public FilterViewModel()
{
CheckFiltersCommand = new DelegateCommand(GetFilters);
GridSource = new List<GridModel>();
GridSource.Add(new GridModel() { Id = 1, Name = "Name1", Country = "Denmark" });
GridSource.Add(new GridModel() { Id = 2, Name = "Name2", Country = "India" });
GridSource.Add(new GridModel() { Id = 3, Name = "Name3", Country = "Australia" });
GridSource.Add(new GridModel() { Id = 4, Name = "Name4", Country = "India" });
GridSource.Add(new GridModel() { Id = 5, Name = "Name5", Country = "Australia" });
GridSource.Add(new GridModel() { Id = 6, Name = "Name6", Country = "Hongkong" });
FilterControlViewModel = new FilterControlViewModel();
FilterControlViewModel.FilterDetails = new List<FilterAttribute>();
foreach (var property in typeof(GridModel).GetProperties())
{
if (property.GetCustomAttributes(true).Where(attr => attr.GetType() == typeof(FilterAttribute)).Any())
{
var attribute = (FilterAttribute)property.GetCustomAttributes(true).Where(attr => attr.GetType() == typeof(FilterAttribute)).First();
FilterControlViewModel.FilterDetails.Add(attribute);
}
}
}
private void GetFilters()
{
FilterCollection = new Dictionary<string, object>();
foreach (var filter in FilterControlViewModel.FilterDetails)
{
if (filter.IsDropDown)
{
if (filter.FilterValue != null)
FilterCollection.Add(filter.FilterKey, filter.FilterValue.GetType().GetProperty("Id").GetValue(filter.FilterValue));
}
else
{
FilterCollection.Add(filter.FilterKey, filter.FilterValue);
}
}
MessageBox.Show(string.Join(", ", FilterCollection.Select(m => m.Key + ":" + Convert.ToString(m.Value)).ToArray()));
}
public List<GridModel> GridSource { get; set; }
public Dictionary<string, object> FilterCollection { get; set; }
public FilterControlViewModel FilterControlViewModel { get; set; }
}
In the above view model 'FilterControlViewModel' property will iterate all property of model and collect the filter information of the properties.
This same property will be assigned to the user control as explained in xaml file below
<Window x:Class="WPFDynamicFilters.GridWithFilters"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPFDynamicFilters"
mc:Ignorable="d"
Title="gridwithfilters" Height="481.239" Width="858.171">
<Grid>
<Grid HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" x:Name="FilterGrid" Height="209" Width="830">
<Border BorderThickness="1" BorderBrush="Gold"/>
<local:Filter DataContext="{Binding FilterControlViewModel, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" Margin="0,0,0,10" />
</Grid>
<DataGrid x:Name="DataGrid" ItemsSource="{Binding GridSource}" HorizontalAlignment="Left" Margin="10,294,0,0" VerticalAlignment="Top" Height="146" Width="830"/>
<Button x:Name="button" Content="Check Filters" HorizontalAlignment="Left" Margin="10,245,0,0" VerticalAlignment="Top" Width="110" Command="{Binding CheckFiltersCommand}"/>
</Grid>
</Window>
Filter control will take all the attributes and render the control using itemscontrol
<UserControl x:Class="WPFDynamicFilters.Filter"
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"
xmlns:local="clr-namespace:WPFDynamicFilters"
mc:Ignorable="d"
d:DesignHeight="40"
>
<UserControl.Resources>
<DataTemplate x:Key="TStringTemplate">
<StackPanel FlowDirection="LeftToRight">
<TextBlock Text="{Binding FilterKey}" />
<TextBox x:Name="TxtFieldValue"
Text="{Binding FilterValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
RenderTransformOrigin="-9.3,0.5" Width="200" FontSize="16" TextAlignment="Left" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="TIntegerTemplate">
<StackPanel FlowDirection="LeftToRight">
<TextBlock Text="{Binding FilterKey}" />
<TextBox x:Name="IntFieldValue"
Text="{Binding FilterValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
RenderTransformOrigin="-9.3,0.5" Width="200" FontSize="16" TextAlignment="Left" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="TDropDownTemplate">
<StackPanel FlowDirection="LeftToRight">
<TextBlock Text="{Binding FilterKey}" />
<ComboBox
SelectedItem="{Binding FilterValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedIndex="{Binding FilterValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
ItemsSource="{Binding ObjectDropDownList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
DisplayMemberPath="Name"
RenderTransformOrigin="-9.3,0.5" Width="200" FontSize="16" />
</StackPanel>
</DataTemplate>
<local:FilterTemplateSelector x:Key="FilterTemplateSelector"
StringTemplate="{StaticResource TStringTemplate}"
IntegerTemplate="{StaticResource TIntegerTemplate}"
DropDownTemplate="{StaticResource TDropDownTemplate}"
/>
</UserControl.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding FilterDetails}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="3" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl
Content="{Binding}"
HorizontalAlignment="Left"
ContentTemplateSelector="{StaticResource FilterTemplateSelector}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>
Finally define the template selector
public class FilterTemplateSelector : DataTemplateSelector
{
public DataTemplate StringTemplate { get; set; }
public DataTemplate IntegerTemplate { get; set; }
public DataTemplate DropDownTemplate { get; set; }
public override DataTemplate SelectTemplate(object item,
DependencyObject container)
{
var filter = (item as FilterAttribute);
if (filter == null) return StringTemplate;
if (!filter.IsDropDown)
{
switch (filter.FilterDataType.Name.ToLower())
{
case "int32":
case "int64":
return IntegerTemplate;
case "string":
return StringTemplate;
}
}
else
{
// display drop down
switch (filter.DropDownList)
{
case "Country":
filter.ObjectDropDownList = GetDropDown.GetCountries().ToList<object>();
break;
}
return DropDownTemplate;
}
return StringTemplate;
}
}