ListView grouping using multiple collections and custom headers - wpf

I have three collections like so:
IReadOnlyList<Info> ListA = {'1A', '1B'}
IReadOnlyList<Info> ListB = {'2A', '2B'}
IReadOnlyList<Info> ListC = {'3A', '3B'}
I am trying to group them, add a custom header to each and show then in a ListView. I tried doing this:
<ListView ItemsSource="{DynamicResource FullCollection}">
<ListView.Resources>
<CollectionViewSource x:Key="CollectionA" Source="{Binding CollectionA}" />
<CollectionViewSource x:Key="CollectionB" Source="{Binding CollectionB}" />
<CollectionViewSource x:Key="CollectionC" Source="{Binding CollectionC}" />
<CompositeCollection x:Key="FullCollection">
<ListViewItem IsEnabled="False">Header_A</ListViewItem>
<CollectionContainer Collection="{Binding Source={StaticResource CollectionA}}" />
<ListViewItem IsEnabled="False">Header_B</ListViewItem>
<CollectionContainer Collection="{Binding Source={StaticResource CollectionB}}" />
<ListViewItem IsEnabled="False">Header_C</ListViewItem>
<CollectionContainer Collection="{Binding Source={StaticResource CollectionC}}" />
</CompositeCollection>
</ListView.Resources>
</ListView>
...
Then i created three seperate ICollectionView objects in viewmodel (CollectionA, CollectionB, CollectionC from ListA, ListB, ListC recpectively) like so:
ICollectionView _collectionA= null;
public ICollectionView CollectionA //from ListA
{
get
{
if (_collectionA== null)
{
_collectionA = CollectionViewSource.GetDefaultView(ListA); //original list
PropertyGroupDescription groupDescription = new PropertyGroupDescription("Header_A");
_collectionA.GroupDescriptions.Add(groupDescription);
}
return _collectionA;
}
}
...
Somehow i am not able to get what I want , what am i doing wrong here!?
How can i group them with custom headers (& expanders) and show then in my ListView, like this:
^ Header_A
--1A
--1B
^ Header_B
--2A
--2B
^ Header_C
--3A
--3B

You should create a view model with an additional property for the group header:
public class InfoViewModel
{
public string Category { get; set; }
public string Name { get; set; }
}
You would then translate your current Info objects to InfoViewModels and group by the new property, e.g.:
var fullSourceCollection = ListA.Select(x => new InfoViewModel() { Category = "Header_A", Name = x.Name })
.Merge(ListB.Select(x => new InfoViewModel() { Category = "Header_B", Name = x.Name })
.Merge(ListC.Select(x => new InfoViewModel() { Category = "Header_C", Name = x.Name })
.ToArray();
...
PropertyGroupDescription groupDescription = new PropertyGroupDescription(nameof(InfoViewModel.Category));
...
Finally, you should define a group style in the view:
<ListView ...>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock FontWeight="Bold" FontSize="14" Text="{Binding Name}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>

Related

Bind ListViewItem ContextMenu MenuItem Command to ViewModel of the ListView's ItemsSource

I have a ListView of Expanders. Each Expander (representing a database table) will have items under it, in another ListView. I want to Right-Click and have an "Edit" option on the innermost items, which represent records in the corresponding database table.
There is an ICommand named 'Edit' in my MainEditorViewModel. The Datacontext in which this command resides is the same as that of the outermost ListView named "TestGroupsListView"
Here is the XAML markup for the ListView of Expanders. The outermost ListView I've named for referencing in the binding via ElementName for the MenuItem's Binding:
<ListView Name="TestGroupsListView" ItemsSource="{Binding TestGroups}" Grid.Row="1">
<ListView.ItemTemplate>
<DataTemplate>
<Expander Style="{StaticResource MaterialDesignExpander}" >
<Expander.Header>
<Grid MaxHeight="50">
<TextBlock Text="{Binding Name}"/>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Add..." Command="{Binding Add}"/>
</ContextMenu>
</Grid.ContextMenu>
</Grid>
</Expander.Header>
<ListView ItemsSource="{Binding Records}" Style="{StaticResource MaterialDesignListView}" Margin="30 0 0 0">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ContextMenu>
<ContextMenu>
<MenuItem Header="Edit"
Command="{Binding ElementName=TestGroupsListView, Path=DataContext.Edit}"
CommandParameter="{Binding }"/>
</ContextMenu>
</Grid.ContextMenu>
<Button Content="{Binding RecordName}" Command="{Binding ElementName=TestGroupsListView, Path=DataContext.Edit}"/>
<!--<TextBlock Text="{Binding RecordName}" AllowDrop="True"/>-->
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
I am able to bind a button in the DataTemplate to 'Edit' successfully, but when I attempt to bind the MenuItem's Command to 'Edit', nothing happens. Why might this be that the button command binding works using ElementName but the same binding in the ContextMenu doesn't?
I think it will be better to use the context menu globally for ListView and globally for each Child ListView. Ok, here is my solution:
<ListBox ItemsSource="{Binding Groups}">
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Add..." Command="{Binding Add}"/>
</ContextMenu>
</ListBox.ContextMenu>
<ListBox.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Name}">
<ListView ItemsSource="{Binding Records}" SelectedItem="{Binding SelectedRecord}">
<ListView.ContextMenu>
<ContextMenu>
<MenuItem Header="Edit" Command="{Binding Edit}" IsEnabled="{Binding CanEdit}"/>
</ContextMenu>
</ListView.ContextMenu>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Expander>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
And for better understanding code behind:
public class GroupsVM : ViewModelBase
{
public ICommand Add
{
get => null; //Command implementation
}
public ObservableCollection<GroupVM> Groups { get; set; } = new ObservableCollection<GroupVM>()
{
new GroupVM { Name = "First" },
new GroupVM { Name = "Second" },
new GroupVM { Name = "Third" }
};
}
public class GroupVM : ViewModelBase
{
private string _name;
public string Name
{
get => _name;
set { _name = value; OnPropertyChanged(); }
}
public ICommand Edit
{
get => null; //Command implementation
}
public bool CanEdit => SelectedRecord != null;
public ObservableCollection<RecordVM> Records { get; set; } = new ObservableCollection<RecordVM>()
{
new RecordVM { Name="Record1" },
new RecordVM { Name="Record2" },
new RecordVM { Name="Record3" }
};
private RecordVM _selectedRecord = null;
public RecordVM SelectedRecord
{
get => _selectedRecord;
set
{
_selectedRecord = value;
OnPropertyChanged();
OnPropertyChanged("CanEdit");
}
}
}
public class RecordVM : ViewModelBase
{
private string _name;
public string Name
{
get => _name;
set { _name = value; OnPropertyChanged(); }
}
}

wpf combobox default value to textBlock

I need to add a default value 'select' in the combobox.I cant add this value to the database.This location value is dynamic.It appears based upon the userrole. I tried different ways nothing worked.Please help.
<ComboBox Width="140" ItemsSource="{Binding SecurityContexts, Mode=OneWay}"
SelectedItem="{Binding ActiveSecurityContext, Mode=TwoWay}"
ToolTip="Working Location">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Location}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
The code behind is
SecurityContexts = new ObservableCollection(_currentUser.ApplicationSecurityContexts);
public interface IApplicationSecurityContext
{
IRole Role { get; }
string Location { get; }
IEnumerable<string> Budgets { get; }
}
public IApplicationSecurityContext ActiveSecurityContext
{
get { return this._currentUser.ActiveSecurityContext; }
set
{
if (this._currentUser.ActiveSecurityContext != value)
{
this._currentUser.ChangeActiveSecurityContext(value);
RaisePropertyChanged("CurrentUser");
LoadData();
}
}
}
You can achieve your goal using CompositeCollection
you can do this. Define resource in your grid/usercontrol/combobox:
<Grid.Resources>
<CollectionViewSource x:Key="cvs" Source="{Binding Binding SecurityContexts, Mode=OneWay}" />
<DataTemplate DataType="{x:Type c:SecurityContexts}">
<TextBlock Text="{Binding Location}"/>
</DataTemplate>
</Grid.Resources>
then your combobox itemsource will be:
<ComboBox.ItemsSource>
<CompositeCollection>
<ComboBoxItem>
<TextBlock Text="select"/>
</ComboBoxItem>
<CollectionContainer Collection="{Binding Source= {StaticResource cvs}}"/>
</CompositeCollection>
</ComboBox.ItemsSource>
It should work. You need to define your datatemplate for your collection in the resource too, to define how your item will be displayed
Note that c in c:SecurityContexts is the path where you defined your custom object

Grouping of items in ListView WPF

I have a ListView where I want to group the items based on a field of the item object. Below is the code I have:
<ListView ItemsSource="{x:Bind MyVM.CollectionOfClassA, Mode=OneWay}"
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock FontSize="15" FontWeight="Bold" Text="{Binding DateTimePropertyOfClassA}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
Is there something I am missing? I want to group the items based on the DateTime property of ClassA objects. Also, if there is no item for any particular day, I would still like to show that date with empty list under that group (for that day). How can I achieve it?
Edit: I am not able to use CollectionViewSource since my VM contains the collection of ClassA object (which is bound as item source to the listview) and I want to group the items based on one property of those ClassA objects. I am sure there is something I am missing out on. But I am not able to figure it out.
Here is the solution for anyone looking for it (thanks to the WPF masters out there https://social.msdn.microsoft.com/Forums/windowsapps/en-US/812ed260-e113-4a8b-9322-226ed56ac90c/grouping-of-items-in-listview-wpf?forum=wpdevelop&prof=required):
public class ClassA
{
public DateTime DateTimePropertyOfClassA { get; set; }
}
public class MyVM
{
public MyVM()
{
//return a grouped collection:
Grouped = from x in CollectionOfClassA group x by x.DateTimePropertyOfClassA into grp orderby grp.Key select grp;
}
public IList<ClassA> CollectionOfClassA { get; set; } = new List<ClassA>()
{
new ClassA(){ DateTimePropertyOfClassA = DateTime.Parse("2016-01-01")},
new ClassA(){ DateTimePropertyOfClassA = DateTime.Parse("2016-03-01")},
new ClassA(){ DateTimePropertyOfClassA = DateTime.Parse("2016-03-01")},
new ClassA(){ DateTimePropertyOfClassA = DateTime.Parse("2016-03-01")},
new ClassA(){ DateTimePropertyOfClassA = DateTime.Parse("2016-03-01")},
new ClassA(){ DateTimePropertyOfClassA =DateTime.Parse("2016-06-01")}
};
public IEnumerable<object> Grouped { get; }
}
Xaml:
<Page.Resources>
<CollectionViewSource x:Name="cvs"
IsSourceGrouped="True"
Source="{x:Bind MyVM.Grouped, Mode=OneWay}"/>
</Page.Resources>
<StackPanel Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ListView ItemsSource="{Binding Source={StaticResource cvs}}">
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock FontSize="15" FontWeight="Bold" Text="{Binding Key}"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
</StackPanel>
I suggest creating a collection in the ViewModel IObservableCollection<ClassA> Properties (or whatever name), add it in the MainWindow class and then simply bind it in the ListView.
<ListView ItemsSource="{Binding Path=Properties}">

WPF ListView grouping by 2 columns but display only 1 group header

A ListView displays a collection of the following class:
public class Employee
{
private string _department;
private string _manager;
private string _name;
private string _address;
public string Department
{
get { return _department; }
}
public string Manager
{
get { return _manager; }
}
public string Name
{
get { return _name; }
}
public string Address
{
get { return _address; }
}
}
There is a 1-to-1 relation between Department and Manager, so any 2 rows with the same department will also have the same manager.
I want to group by Department/Manager, with the group header showing "Department (Manager)".
My CollectionViewSource looks like
<CollectionViewSource x:Key="cvsEmployees" Source="{Binding Employees}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Department" />
<PropertyGroupDescription PropertyName="Manager" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
The plan is to not display the first level header (Department) and to somehow bind to both the Department (1st level) and the Manager (2nd level) from the 2nd level header.
3 questions:
To avoid displaying the 1st level header, I have an empty data template in the groupstyle:
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
This seems very clunky. Is there a more elegant way to skip a group header?
How do I bind to the 1st grouping level property (Department) from the 2nd level header (Manager) to achieve the required "Department (Manager)" ?
Is there a better way to do this than creating 2 grouping level?
Thanks
Solved the main stumbling block, question 2 above: how to bind from the group header to a property that is not the grouping property.
The solution is to change the data context to:{Binding Items}. The ItemSource properties are then available
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0,10,0,3" DataContext="{Binding Items}" >
<TextBlock Text="{Binding Path=Department}" FontWeight="Bold" Margin="3"/>
<TextBlock Text="{Binding Path=Manager, StringFormat='({0})'}" Margin="3"/>
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
I would create another model part, which represents the dual grouping that you need to have happen:
Model Classes:
public class EmployeeModel {
private readonly Employee _Employee;
public DepartmentManager ManagementInfo { get; private set; }
public string Name {
get { return _Employee.Name; }
}
public string Address {
get { return _Employee.Address; }
}
public EmployeeModel(Employee employee) {
this._Employee = employee;
this.ManagementInfo = new DepartmentManager(employee.Department, employee.Manager);
}
}
public class DepartmentManager {
public string Department { get; private set; }
public string Manager { get; private set; }
public DepartmentManager(string dept, string manager) {
this.Department = dept;
this.Manager= manager;
}
public override bool Equals(object obj) {
var model = obj as DepartmentManager;
if(null == model)
return false;
return Department.Equals(model.Department, StringComparison.InvariantCultureIgnoreCase) &&
Manager.Equals(model.Manager, StringComparison.InvariantCultureIgnoreCase);
}
}
XAML:
<CollectionViewSource x:Key="cvsEmpsModel" Source="{Binding EmployeesModel}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="ManagementInfo" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
<DataTemplate DataType="{x:Type models:EmployeeModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Address}" />
</StackPanel>
</DataTemplate>
...
<ListView ItemsSource="{Binding Source={StaticResource cvsEmpsModel}}">
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0,10,0,3" DataContext="{Binding Items}">
<TextBlock Text="{Binding Path=ManagementInfo.Manager}" FontWeight="Bold" Margin="3" />
<TextBlock Text="{Binding Path=ManagementInfo.Department, StringFormat='({0})'}" Margin="3" />
</StackPanel>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
Then in your Window/ViewModel:
this.EmployeesModel = new ObservableCollection<EmployeeModel>(MyListOfEmployersFromDB.Select(e => new EmployeeModel(e)));
Note, I've overriden Equals in the DepartmentManager class, but not GetHashCode, ideally you should do a custom implementation of that. I had to override equals so the grouping view source would correctly group the same entries. You could get rid of this need, buy constructing the DepartmentManager for the same Employees outside of the collection, and pass them into the EmployeeModel ctr.

How to bind two ObservableCollection(s) to one ListBox?

Does anybody know how to binding two ObservableCollections in one ListBox?
these two ObservableCollections both have a Property string "name" to display in the ListBox,
int the ListBox top area, will display the ObservableCollection1 items and in the ListBox bottom area I want display the ObservableCollection2 items, how to do that?
<ListBox x:Name="m_CtrlMediaList" Grid.Column="2" AllowDrop="True" SelectionMode="Extended">
<ListBox.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding directorys}"/>
<CollectionContainer Collection="{Binding files}"/>
</CompositeCollection>
</ListBox.ItemsSource>
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding name, Mode=OneWay}" FontWeight="Bold" FontSize="14"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
the data is like:
class ElementFile
{
...
string name (get;set;}
...
}
class ElementDirectory
{
...
string name (get;set;}
...
public ObservableCollection<ElementDirectory> directorys { get; set; }
public ObservableCollection<ElementFile> files { get; set; }
...
}
Why can not display the "name"?
The composite collection is what you are looking for. Example.

Resources