I'm trying to display on datagrid name and sex of a person. User should be able to enter name and select sex from combobox. I want combobox to be visible all the time, so I'm using DataGridTemplateColumn. However whenever I add new person to list, sex is not displayed on combobox but I'm sure it is set. But if I select anything from combobox, selection will be shown, and data on model will be updated. Is there anything to make this work? Notice: I'm using Caliburn.
This is my View:
<Window x:Class="WpfPetApp.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<DataGrid x:Name="TestDataGrid" Grid.Row="0" ColumnWidth="*" HeadersVisibility="Column" SelectedItem="{Binding SelectedTestDataGrid, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
CanUserDeleteRows="False"
CanUserAddRows="False"
CanUserResizeColumns="False"
CanUserReorderColumns="False"
CanUserResizeRows="False"
CanUserSortColumns="True"
AutoGenerateColumns="False"
SelectionMode="Single" >
<DataGrid.Columns>
<DataGridTextColumn Header="Name" Binding="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></DataGridTextColumn>
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ComboBox ItemsSource="{Binding ElementName=TestDataGrid, Path=DataContext.Sexes}" SelectedValue="{Binding Sex, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
<Button x:Name="AddRow" Grid.Row="1">Add</Button>
</Grid>
</Window>
ViewModel:
public class ShellViewModel : PropertyChangedBase, IShell
{
private Person _selectedTestDataGrid;
public ObservableCollection<Person> TestDataGrid { get; set; }
public ObservableCollection<Sex> Sexes { get; set; }
public Person SelectedTestDataGrid
{
get { return _selectedTestDataGrid; }
set
{
if (Equals(value, _selectedTestDataGrid)) return;
_selectedTestDataGrid = value;
NotifyOfPropertyChange(() => SelectedTestDataGrid);
}
}
public ShellViewModel()
{
TestDataGrid = new ObservableCollection<Person>();
Sexes = new ObservableCollection<Sex>();
Sexes.Add(Sex.Male);
Sexes.Add(Sex.Female);
}
public void AddRow()
{
var rnd = new Random(DateTime.Now.Millisecond);
var male = rnd.Next() % 2 == 0;
TestDataGrid.Add(new Person { Name = Guid.NewGuid().ToString(), Sex = (male ? Sex.Male : Sex.Female) });
}
}
And model:
public class Person
{
public string Name { get; set; }
public Sex Sex { get; set; }
}
public class Sex
{
public static readonly Guid MaleSexId = new Guid("9BF621E2-98FC-45FB-A4AC-4A930AD95CF7");
public static readonly Guid FemaleSexId = new Guid("425B1B2A-B47D-4C25-A5AF-D3A0ABC95A75");
public static Sex Male
{
get
{
return new Sex(MaleSexId);
}
}
public static Sex Female
{
get
{
return new Sex(FemaleSexId);
}
}
private readonly Guid _sexId;
public Sex(Guid sexId)
{
_sexId = sexId;
}
public Guid Value
{
get { return _sexId; }
}
public override string ToString()
{
return _sexId == MaleSexId ? "male" : _sexId == FemaleSexId ? "female" : "unknown";
}
}
And a screen of app: https://www.dropbox.com/s/fi1ef3z07vb09yw/2014-04-26%2013_07_22-.png
Any help will be appreciated.
No item is selected on addition of row because ItemsSource and SelectedValue refers to completely different instances.
I don't know why you are always returning new instance every time Sex.Male and Sex.Female is accessed. So, when ItemsSource gets set, new instance of Male and Female gets added and when you add row, new instance of Male/Female is returned which is not there in ItemSource and hence no item was selected.
Either set Sex property to already added instance in ComboBox like this:
TestDataGrid.Add(new Person { Name = Guid.NewGuid().ToString(),
Sex = (male ? Sexes.First() : Sexes.Last()) });
OR
Always return same instance from Male/Female:
public class Sex
{
private static Sex male = new Sex(MaleSexId);
public static Sex Male
{
get
{
return male;
}
}
private static Sex female = new Sex(FemaleSexId);
public static Sex Female
{
get
{
return female;
}
}
.....
}
Ideally Sex should have been Enum, no need of it having a class for it.
public enum Sex
{
Male,
Female
}
Related
I have a datagrid which is bound to my ObservableCollection.
Each column is bound to property of the class wellenelement.
Now i would like to convert the column "Art" to a combobox column, where the user can choose from 3 different options.
How can i create these 3 Combobox items and add it to the datagrid?
<DataGrid AutoGenerateColumns="True" Name="dataGrid1" ItemsSource="{Binding}" >
</DataGrid>
```xaml
```c#
public partial class MainWindow : Window
{
public ObservableCollection<Wellenelement> Welle1;
public MainWindow()
{
InitializeComponent();
Welle1 = new ObservableCollection<Wellenelement>();
dataGrid1.DataContext = Welle1;
}
}
```c#
```c#
public class Wellenelement
{
public string Art { get; set; }
public string UK { get; set; }
public string DA { get; set; }
public string DI { get; set; }
}
```c#
If it's the same for each item you can add a collection to the viewmodel and add it as a resource to your view. Then you can set the resource as the itemssource for the DataGridComboBoxColumn.
ViewModel:
public class MainViewModel
{
public MainViewModel()
{
Wellenelements = new ObservableCollection<Wellenelement>()
{
new Wellenelement()
{
UK = "uk1",
DA = "da1",
DI = "di1"
},
new Wellenelement()
{
UK = "uk2",
DA = "da2",
DI = "di2"
},
};
ArtTypes = new List<string>()
{
"new art","old art", "good art","bad art"
};
}
public ObservableCollection<Wellenelement> Wellenelements { get; set; }
public List<string> ArtTypes { get; set; }
}
View:
<Window ...
xmlns:viewmodels="clr-namespace:WpfApp.Viewmodels"
...>
<Window.DataContext>
<viewmodels:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<CollectionViewSource x:Key="myCollection" Source="{Binding ArtTypes}"/>
</Window.Resources>
<Grid>
<DataGrid AutoGenerateColumns="False"
CanUserAddRows="False"
ItemsSource="{Binding Wellenelements}" >
<DataGrid.Columns>
<DataGridComboBoxColumn ItemsSource="{Binding Source={StaticResource myCollection}}"
SelectedItemBinding="{Binding Art, UpdateSourceTrigger=PropertyChanged}"
Header="Art Combo column"/>
<DataGridTextColumn Binding="{Binding Art}"
IsReadOnly="True"
Header="Selected Art Type"/>
<DataGridTextColumn Binding="{Binding UK}"
Header="UK"/>
<DataGridTextColumn Binding="{Binding DA}"
Header="DA"/>
<DataGridTextColumn Binding="{Binding DI}"
Header="DI"/>
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>
Wellenelement class:
public class Wellenelement : ObservableObject
{
public string UK { get; set; }
public string DA { get; set; }
public string DI { get; set; }
private string _art;
public string Art
{
get { return _art; }
set
{
_art = value;
OnPropertyChanged(nameof(Art));
}
}
}
I'm having a problem with grouping a CollectionView which is bound to an ItemsControl.
In my ViewModel I have multiple groups. A Group is a collection of Person, and each Person knows which group it belongs to.
See Group and Person classes at the end.
- Here's what I want to do : I want to be able to show a collection of Person(s) grouped by their groups. I emphesize that I want to create these groups in the view from a collection of Persons, not from a collection of Groups.
- Here's how I do it :
I have a list which contains every Person across all groups (the list is called "Everyone"), and I have a CollectionView which I create from Everyone called "EveryoneGrouped". I then add a PropertyGroupDescription created from "Group" to my EveryoneGrouped collection.
- Here's my problem :
Groups appear multiple times, in fact groups appear as many times as the amount of Person they contain.
If I have the following groups :
- Group ONE ["Alpha One"]
- Group TWO ["Alpha Two", "Beta Two"]
- Group THREE ["Alpha Three", "Beta Three", "Gamma Three"]
It will produce the following :
Group ONE appears only once since it contains only one Person. I would like for groups TWO and THREE to appear only once as well.
I just can't put my finger on what I'm doing wrong, any help would be greatly appreciated.
Edit : multiple Groups or Persons can have the same name.
Here's my code which produces the previous screenshot :
ViewModel
public class ViewModel
{
public CollectionView EveryoneGrouped { get; private set; }
private List<Person> Everyone { get; set; } = new List<Person>();
private List<Group> AllGroups { get; set; } = new List<Group>();
public ViewModel()
{
populateGroups();
populateEveryoneCollection();
createCollectionView();
}
private void populateGroups()
{
Group one = new Group
{
new Person { PersonName = "Alpha One" }
};
one.GroupName = "ONE";
Group two = new Group
{
new Person { PersonName = "Alpha Two" },
new Person { PersonName = "Beta Two" }
};
two.GroupName = "TWO";
Group three = new Group
{
new Person { PersonName = "Alpha Three" },
new Person { PersonName = "Beta Three" },
new Person { PersonName = "Gamma Three" }
};
three.GroupName = "THREE";
AllGroups.Add(one);
AllGroups.Add(two);
AllGroups.Add(three);
}
private void populateEveryoneCollection()
{
foreach(Group group in AllGroups)
{
foreach(Person person in group)
{
Everyone.Add(person);
}
}
}
private void createCollectionView()
{
EveryoneGrouped = (CollectionView)CollectionViewSource.GetDefaultView(Everyone);
PropertyGroupDescription groupDescription = new PropertyGroupDescription("Group");
EveryoneGrouped.GroupDescriptions.Add(groupDescription);
}
}
XAML
<Window x:Class="FunWithCollectionViewGrouping.MainWindow"
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:FunWithCollectionViewGrouping"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ItemsControl ItemsSource="{Binding EveryoneGrouped}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding PersonName}" Margin="5,0"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock DockPanel.Dock="Left" FontWeight="Bold" Text="{Binding Name.Group.GroupName}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ItemsControl.GroupStyle>
</ItemsControl>
</Grid>
Code Behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
Person
public class Person
{
private static int PersonQuantity = 0;
private int _id = ++PersonQuantity;
public int Id { get { return _id; } }
public Group Group { get; set; }
public string PersonName { get; set; }
}
Group
public class Group : ObservableCollection<Person>
{
private static int GroupQuantity = 0;
private int _id = ++GroupQuantity;
public int Id { get { return _id; } }
public string GroupName { get; set; }
public Group()
{
CollectionChanged += Group_CollectionChanged;
}
private void Group_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if( e.NewItems != null )
{
foreach(Person p in e.NewItems)
{
if ( p.Group != null ) { p.Group.Remove(p); }
p.Group = this;
}
}
if( e.OldItems != null )
{
foreach (Person p in e.OldItems)
{
if (p.Group != null && p.Group.Equals(this)) { p.Group = null; }
}
}
}
public override bool Equals(object obj)
{
Group other = obj as Group;
if (obj == null) return false;
return Id == other.Id;
}
public override int GetHashCode()
{
return Id;
}
}
Thank you.
Group by the GroupName property of the Group:
private void createCollectionView()
{
EveryoneGrouped = (CollectionView)CollectionViewSource.GetDefaultView(Everyone);
PropertyGroupDescription groupDescription = new PropertyGroupDescription("Group.GroupName");
EveryoneGrouped.GroupDescriptions.Add(groupDescription);
}
And display the name of the group:
<ItemsControl.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock DockPanel.Dock="Left" FontWeight="Bold" Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ItemsControl.GroupStyle>
The problem I have with this solution is that multiple groups can have the same name (I updated my post to specify this), and they will be merged in the view if I do this. Groups are distinguished by their ID (which I can't sort by because then I would loose the group name in the view).
Group by the Id property then:
private void createCollectionView()
{
EveryoneGrouped = (CollectionView)CollectionViewSource.GetDefaultView(Everyone);
PropertyGroupDescription groupDescription = new PropertyGroupDescription("Group.Id");
EveryoneGrouped.GroupDescriptions.Add(groupDescription);
}
And bind the TextBlock to the GroupName property of the Group of the first item in the group:
<ItemsControl.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock DockPanel.Dock="Left" FontWeight="Bold" Text="{Binding Items[0].Group.GroupName}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ItemsControl.GroupStyle>
What I don't understand is why it behaves this way, doesn't PropertyGroupDescription use Equals and GetHashCode?
Obviously not. It uses reflection to be able to group by any property specified by a string.
I am new to WPF application development. I want to get floors on the basis of blocks. I have one combo box of blocks and another combo box of floors. When I select any block in one combo box, the other combo box should display the floors of the select block.
This is the combo box layout:
<ComboBox Grid.Row="0" Grid.Column="0" Width="100"
Margin="0,0,0,10" Height="35"
Loaded="FrameworkElement_OnLoaded"
SelectedValuePath ="Id"
SelectedValue ="{Binding SelectedBlockId, Mode=TwoWay}"
DisplayMemberPath="Name"
SelectionChanged="Selector_OnSelectionChanged" ItemsSource="{Binding Blocks}" />
<ComboBox Grid.Row="0" Grid.Column="3" Width="100"
Margin="0,0,0,10" Height="35"
Loaded="FrameworkElement_OnLoaded"
SelectedValuePath ="Id"
SelectedValue ="{Binding SelectedFloorId, Mode=TwoWay}"
DisplayMemberPath="Name"
SelectionChanged="Selector_OnSelectionChanged" ItemsSource="{Binding Floors}" />
Here's a slightly different example.
Xaml...
<!-- language: xaml -->
<Label Name="FavoriteFoodLbl" Grid.Column="0" Grid.Row="13">Favorite Food</Label>
<ComboBox Name="FavoriteFoodCombo" Grid.Column="1" Grid.Row="13" ItemsSource="{Binding Foods}" SelectedItem="{Binding FavoriteFood, UpdateSourceTrigger=PropertyChanged}" />
<Label Name="FavoriteFlavourLbl" Grid.Column="2" Grid.Row="13">Favorite Flavour</Label>
<ComboBox Name="FavoriteFlavourCombo" Grid.Column="3" Grid.Row="13" ItemsSource="{Binding Flavours}" />
View model/code-behind code...
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<string> Foods { get; set; } = new ObservableCollection<string>() { "Pizza", "Ice Cream", "Soup" };
private string _favoriteFood;
public string FavoriteFood
{
get { return _favoriteFood; }
set
{
_favoriteFood = value;
switch (_favoriteFood)
{
case "Pizza":
_flavours = new ObservableCollection<string>(PizzaToppings);
break;
case "Ice Cream":
_flavours = new ObservableCollection<string>(IceCreamFlavours);
break;
case "Soup":
_flavours = new ObservableCollection<string>(SoupFlavours);
break;
default:
_flavours = new ObservableCollection<string>();
break;
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("FavoriteFood"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Flavours"));
}
}
public List<string> PizzaToppings { get; set; } = new List<string>() { "Margarita", "Pepperoni", "Meat Feast" };
public List<string> IceCreamFlavours { get; set; } = new List<string>() { "Vanilla", "Strawberry", "Chocolate" };
public List<string> SoupFlavours { get; set; } = new List<string>() { "Tomato", "Leek and Potato", "Chicken" };
private ObservableCollection<string> _flavours = null;
public ObservableCollection<string> Flavours
{
get
{
return _flavours;
}
set
{
_flavours = value;
RaisePropertyChanged();
}
}
public string FavoriteFlavour { get; set; }
Change out the case statement for something appropriate.
Does this help?
What you need to do is to establish master-detail relationship between Block and Floor models and correctly bind them to you View (comboboxes).
So supposing your Floor and Block have two properties ID and Description, your model should look like this:
public class Floor
{
public int Id { get; set; }
public string Description { get; set; }
}
public class Block
{
public int Id { get; set; }
public int Description { get; set; }
// notice Floors collection inside each block
public IList<Floor> Floors { get; set; }
public Block()
{
Floors = new List<Floor>();
}
}
Your ViewModel will contain two ObservableCollections and one property to store currently selected block. Notice SelectedBlock property setter: when the property is updated, the Floors collection gets recreated with new values.
public const string BlocksPropertyName = "Blocks";
private ObservableCollection<Block> _blocks = null;
public ObservableCollection<Block> Blocks
{
get
{
return _blocks;
}
set
{
_blocks = value;
RaisePropertyChanged(BlocksPropertyName);
}
}
public const string SelectedBlockPropertyName = "SelectedBlock";
private Block _selectedBlock = null;
public Block SelectedBlock
{
get
{
return _selectedBlock;
}
set
{
_selectedBlock = value;
RaisePropertyChanged(SelectedBlockPropertyName);
if (_selectedBlock != null)
{
Floors = new ObservableCollection<Floor>(_selectedBlock.Floors);
}
}
}
public const string FloorsPropertyName = "Floors";
private ObservableCollection<Floor> _floors = null;
public ObservableCollection<Floor> Floors
{
get
{
return _floors;
}
set
{
_floors = value;
RaisePropertyChanged(FloorsPropertyName);
}
}
In your XAML you just bind both ComboBoxes to corrispective collections:
<ComboBox ItemsSource="{Binding Blocks}"
SelectedItem="{Binding SelectedBlock}" />
<ComboBox ItemsSource="{Binding Floors}" />
Please refer to the following blog post for an example of how to implement cascading ComboBoxes in WPF using the MVVM pattern: https://blog.magnusmontin.net/2013/06/17/cascading-comboboxes-in-wpf-using-mvvm/
You could basically just replace the Countries and the Cities types from the sample code with your Block and Floor types.
I was trying to write a sample code to test out the question asked # Binding a grid with two datasources in silverlight
I have a XAML as
<Grid x:Name="LayoutRoot" Background="White">
<sdk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding WrapperClass}">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn Header="Name" Binding="{Binding People.Name, Mode=TwoWay}"/>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
</Grid>
My view model code
private ItemWrapper _wrapperClass;
public ItemWrapper WrapperClass
{
get
{
if (_wrapperClass == null)
_wrapperClass = new ItemWrapper();
return _wrapperClass;
}
}
The item wrapper and person class definition
public class ItemWrapper
{
private ObservableCollection<Person> _people;
public ObservableCollection<Person> People
{
get
{
return _people;
}
}
public ItemWrapper()
{
_people = new ObservableCollection<Person>();
_people.Add(new Person { ID = 1, Name = "Name1", JobTitle = 1 });
_people.Add(new Person { ID = 2, Name = "Name2", JobTitle = 2 });
}
}
public class Person
{
public int ID { get; set; }
public string Name { get; set; }
public int JobTitle { get; set; }
}
When I run the program if binding property works, I was expecting names to show in the grid but it does not. It just shows the header. Am I missing anything here?
Thanks,
Try this..
<Grid x:Name="LayoutRoot" Background="White">
<sdk:DataGrid AutoGenerateColumns="False" ItemsSource="{Binding WrapperClass.People}">
<sdk:DataGrid.Columns>
<sdk:DataGridTextColumn Header="Name" Binding="{Binding Name, Mode=TwoWay}"/>
</sdk:DataGrid.Columns>
</sdk:DataGrid>
Cheers.
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;
}
}