Binding ListView field to a nested list WPF - c#

I've the following classes:
class Event {
int eserc {get;set;}
int type {get;set;}
}
class Sequence {
List<Event> events;
int freq {get;set;}
}
As you can see, I have a list of events inside a Sequence. I have a list of Sequence.I want to show a ListView with a GridView with the list of sequences. For each sequence I want to have 2 columns, one with the value of the property freq and the other one should have the list of events associated with that sequence. For example:
where the first line is related to the first sequence. The color of the rectangle represents the event's type. In the first sequence there are the following events:
eserc 1 of type "red"
eserc 2 of type "red"
eserc 3 of type "green"
eserc 4 of type "red"
I know that I have to do the binding to display values, but I don't know how to do it for the sequences, because I should bind the value of the column to the values of the Event objects within each single Sequence.That's the code that I wrote for the ListView:
<ListView Name="resultsList" Grid.Row="5" Grid.Column="1"
Grid.ColumnSpan="3">
<ListView.View>
<GridView>
<GridViewColumn Header="Sequence" Width="450"
DisplayMemberBinding="{Binding events}"/>
<GridViewColumn Header="Frequence"
DisplayMemberBinding="{Binding freq}"/>
</GridView>
</ListView.View>
</ListView>
Of course, Binding events is wrong because that would work only if it was a string, but that's the idea.
I searched on internet and I think that I should use something like DataTemplate, but I'm not sure about that and I didn't understand well how that works. I understood that it works when the source is an object, but in this case it's a List of objects and I don't know how to get the information.

To achieve that you need to define another list inside the first GridViewColumn, the list should be horizontal (Edit the ItemsPanelTemplate). You could either a ListView, ListBox or an ItemsControl (looks like the most appropriate).
To draw a Border with the different colors based on the Event's type, you should first, define a custom DataTemplate for the ItemsControl items, and use a DataTrigger to set the color, here the full xaml to do that:
<ListView Name="ResultsList"
ItemsSource="{Binding SequenceCollection}">
<ListView.View>
<GridView>
<GridViewColumn Header="Sequence" Width="450" >
<GridViewColumn.CellTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding Events}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border>
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<DataTrigger Binding="{Binding Type}" Value="red">
<Setter Property="Background" Value="red"/>
</DataTrigger>
<DataTrigger Binding="{Binding Type}" Value="green">
<Setter Property="Background" Value="Green"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding Eserc, StringFormat='{}{0} '}"></TextBlock>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Frequence"
DisplayMemberBinding="{Binding Freq}"/>
</GridView>
</ListView.View>
</ListView>
The SequenceCollection:
private ObservableCollection<Sequence> _sequenceCollection =new ObservableCollection<Sequence>()
{
new Sequence(){Events = new ObservableCollection<Event>()
{
new Event(){Eserc=1, Type = "red"},
new Event(){Eserc=2, Type = "red"},
new Event(){Eserc=3, Type = "green"},
new Event(){Eserc=4, Type = "red"},
},Freq = 3}
};
public ObservableCollection<Sequence> SequenceCollection
{
get { return _sequenceCollection; }
set
{
if (Equals(value, _sequenceCollection)) return;
_sequenceCollection = value;
OnPropertyChanged();
}
}
And here are you classes with the needed adjacements:
public class Event
{
public int Eserc { get; set; }
public string Type { get; set; }
}
public class Sequence
{
public ObservableCollection<Event> Events { get; set; }
public int Freq { get; set; }
}
Output:
On the side:
make sure to define public properties to be able to bind them correctly
use the naming convention
use ObservableCollection instead of List (they implement ICollectionChanged which is handy for change notifications)
don't forget to implement the INotifyPropertyChanged Interface

Related

How to trigger converter when ObservableCollection is changed in WPF using MVVM?

I have two list like this:
<ListView Name="listView" Height="300" ItemsSource="{Binding Resultados}">
<ListView.View>
<GridView>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox
IsChecked="{Binding Path=CommandParameter, RelativeSource={RelativeSource Self}, Converter={StaticResource CheckBoxCeldaConverter}, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}"
Command="{Binding Path=DataContext.SeleccionarCeldaCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
CommandParameter="{Binding}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="EDIFICIO" DisplayMemberBinding="{Binding NIVEL.PASILLO.EDIFICIO.NOMBRE}"/>
<GridViewColumn Header="PASILLO" DisplayMemberBinding="{Binding NIVEL.PASILLO.NOMBRE}"/>
<GridViewColumn Header="NIVEL" DisplayMemberBinding="{Binding NIVEL.NOMBRE}"/>
<GridViewColumn Header="CELDA" DisplayMemberBinding="{Binding CELDA.NOMBRE}"/>
<GridViewColumn Header="CLASIFICACIÓN" DisplayMemberBinding="{Binding CELDA_CATEGORIA.NOMBRE}"/>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button
Command="{Binding Path=DataContext.MostrarDiasVisitasCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
CommandParameter="{Binding CELDA.CELDA_DIAS_VISITA}"
IsEnabled="{Binding TieneDiasVisita}">
<materialDesign:PackIcon Kind="Eye"/>
</Button>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<ListView Name="listView2" Height="300" ItemsSource="{Binding CeldasSeleccionadas}" Grid.Column="1" Margin="20 0 0 0">
<ListView.View>
<GridView>
<GridViewColumn Header="EDIFICIO" DisplayMemberBinding="{Binding NIVEL.PASILLO.EDIFICIO.NOMBRE}"/>
<GridViewColumn Header="PASILLO" DisplayMemberBinding="{Binding NIVEL.PASILLO.NOMBRE}"/>
<GridViewColumn Header="NIVEL" DisplayMemberBinding="{Binding NIVEL.NOMBRE}"/>
<GridViewColumn Header="CELDA" DisplayMemberBinding="{Binding CELDA.NOMBRE}"/>
<GridViewColumn Header="CLASIFICACIÓN" DisplayMemberBinding="{Binding CELDA_CATEGORIA.NOMBRE}"/>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button
Command="{Binding Path=DataContext.BorrarCeldaCommand, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
CommandParameter="{Binding}">
<materialDesign:PackIcon Kind="Delete"/>
</Button>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
In the first list, the elements have a CheckBox, if the CheckBox is pressed, the element is added or removed from the second list that it's ItemSource is binded to CeldasSeleccionadas ObservableCollection in the ViewModel. I have a converter in the binding of the isChecked property of the first list that checks if the element is present in the CeldasSeleccionadas ObservableCollection, if is present, put the isChecked property as true. This is because if the user update the View, the selected values keeps checked.
Now, in the second list, the one binded to CeldasSeleccionadas, I have a button to delete the element from CeldasSeleccionadas ObservableCollection. This works fine, the only problem I have is that when I remove the element from CeldasSeleccionadas ObservableCollection the CheckBox in the first list keeps isChecked property as true, the converter is not beign called again and is not checking if the element exist in the CeldasSeleccionadas ObservableCollection.
CeldasSeleccionadas looks like this in the ViewModel:
public ObservableCollection<CeldaModel> CeldasSeleccionadas { get; set; } = new ObservableCollection<CeldaModel>();
This is the Command to add elements to CeldasSeleccionadas:
private ICommand _SeleccionarCeldaCommand;
public ICommand SeleccionarCeldaCommand {
get {
if (_SeleccionarCeldaCommand == null) _SeleccionarCeldaCommand = new RelayCommand(param =>SeleccionarCelda((CeldaModel) param));
return _SeleccionarCeldaCommand;
}
}
void SeleccionarCelda(CeldaModel celda) {
if (CeldasSeleccionadas.Contains(celda)) {
CeldasSeleccionadas.Remove(celda);
}
else {
CeldasSeleccionadas.Add(celda);
}
}
And this for remove elements:
private ICommand _BorrarCeldaCommand;
public ICommand BorrarCeldaCommand {
get {
if (_BorrarCeldaCommand == null) _BorrarCeldaCommand = new RelayCommand(param =>BorrarCelda((CeldaModel) param));
return _BorrarCeldaCommand;
}
}
void BorrarCelda(CeldaModel celda) {
CeldasSeleccionadas.Remove(celda);
}
The Converter looks like this:
public class CheckBoxCeldaConverter : Freezable, IValueConverter
{
public CheckBoxCeldaConverter()
{
MyCollection = new ObservableCollection<CeldaModel>();
}
protected override Freezable CreateInstanceCore()
{
return new CheckBoxCeldaConverter();
}
public static readonly DependencyProperty MyCollectionProperty =
DependencyProperty.Register(nameof(MyCollection),
typeof(ObservableCollection<CeldaModel>), typeof(CheckBoxCeldaConverter),
new FrameworkPropertyMetadata(null));
public ObservableCollection<CeldaModel> MyCollection
{
get { return GetValue(MyCollectionProperty) as ObservableCollection<CeldaModel>; }
set { SetValue(MyCollectionProperty, value); }
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (MyCollection.Contains(value as CeldaModel))
{
return true;
}
else
{
return false;
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
And I call it from my view inside the User.Resources like this:
<UserControl.Resources>
<local:CheckBoxCeldaConverter MyCollection="{Binding CeldasSeleccionadas}" x:Key="CheckBoxCeldaConverter"/>
</UserControl.Resources>
How can I trigger the converter when a item from the CeldasSeleccionadas ObservableCollection is added or removed?
Read all, it's more intuitive and explained than before.
The code will be much easier if CeldaModel was aware of "I'm selected", letting you bind the checkbox to "Selected" boolean property.
With that you can avoid the converter.
But I think there is a better choice.
You have "Resultados".
Then in you ViewModel creates two more properties ICollectionView
public class MasterViewModel : ViewModelBase //example{
ObservableCollection<CeldaModel> Resultados{get; set;}
ICollectionView TodosResultados{get; set;}
ICollectionView ResultadosSeleccionados {get; set;} //You create ICollectionView and filter it by "Selected" property in CeldaModel
...
}
Bind one listview to "TodosResultados" and the other to "ResultadosSeleccionados"
If you have bound everything correctly (checkbox with Selected, ListViews with ICollectionView), you don't have to worry about nothing. Everything will work like a charm. At least in the surface.
No need to create the "Commands" (unless the command make something in the back Model- don't mess up with the ViewModel).
Warning: this work fine if you only want "View", if you want two ObservableCollection is valid too.
PS: should you name "CeldaModel" "CeldaViewModel"? In a pure MVVM, Model classes must not interact in any way with the view, even if you include it in a collection.

ItemSource value can evaluate to decide the style between label or radio buttons

I am looking for a wpf example of when Itemsource is more than 1 then it can creates radio button dynamically. If it just one value in Itemsource then it should be label instead of radio buttons. Do I have to write converter which can pass the value of Itemsource and control the style between label and radio button. If anyone can provide example then it will be great.
You may use a ItemContainerStyle with a DataTrigger on the Count property of the ItemsControl's Items.
The Style would chose between two different ContentTemplates, depending on whether there is exactly one item or not.
<ItemsControl ItemsSource="{Binding ...}">
<ItemsControl.Resources>
<DataTemplate x:Key="DefaultItemTemplate">
<RadioButton Content="{Binding ...}"/>
</DataTemplate>
<DataTemplate x:Key="SingleItemTemplate">
<Label Content="{Binding ...}"/>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="ContentTemplate"
Value="{StaticResource DefaultItemTemplate}"/>
<Style.Triggers>
<DataTrigger
Binding="{Binding Items.Count,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"
Value="1">
<Setter Property="ContentTemplate"
Value="{StaticResource SingleItemTemplate}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
The ContentControl creates a control and chooses the correct DataTemplate for it based on its Type. So you don't need a converter:
<ContentControl Content="{Binding Child}">
<ContentControl.Resources>
<DataTemplate DataType="{x:Type local:Type1}">
<Label Content="{Binding ItemText}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Type2}">
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding ItemText}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</ContentControl.Resources>
</ContentControl>
This code reacts to different types of Child property inside VM:
public BaseType Child { get; set; }
Data types:
public class BaseType { }
public class Type1 : BaseType
{
public string ItemText { get; set; }
}
public class Type2 : BaseType
{
public ObservableCollection<Type1> Items { get; } = new ObservableCollection<Type1>();
}
for example if you set Child to a Type1 it will show you one label
Child = new Type1 { ItemText = "hello" };
or if you set it to Type2 you will have your radiobutton collection:
var t = new Type2();
t.Items.Add(new Type1 { ItemText = "hello" });
t.Items.Add(new Type1 { ItemText = "world" });
Child = t;

WPF ObservableCollection containing List<T>

for some time I am struggling with ObservableCollection containing List. I am not able to get it working in a way, that each List is in its own expander.
You can imagine input data as a list of records per day and each list contains orders for that day.
Here is some code:
My version 1:
public class Order
{
public string OrderId { get; private set}
}
public ObservableCollection<Order> ObservableResults
{
get
{
return new ObservableCollection<Order>
{
new Order("Foo"),
new Order("Bar")
};
}
}
XAML file:
<ScrollViewer>
<ListView Background="Transparent" ItemsSource="{Binding ObservableResults}">
<ListView.View>
<GridView>
<GridViewColumn Header="Order Id" Width="650" DisplayMemberBinding="{Binding OrderId, UpdateSourceTrigger=PropertyChanged}" />
</GridView>
</ListView.View>
/ListView>
</ScrollViewer>
This one worked just fine, but the problem was, that I had all the orders in a single grid view and that's something I did not really like so I wanted to modify it, that I will get List of orders and fill the ObservableCollection with these lists. Pretty much each list represents one day worth of orders. So I have modified the code like this:
Code behind
public class Order
{
public string OrderId { get; private set}
}
public ObservableCollection<List<Order>> ObservableResults
{
get
{
return new ObservableCollection<Order>
{
new List<Order>
{
new Order("Foo")
},
new List<Order>
{
new Order("Bar")
}
};
}
}
But I do have a problem with the XAML part right now... I am not really sure, how to achieve the following:
It will be a single scroll view, where each List<Order> will be encapsulated in its own Expander (so I can open close each day individually). So far I have this:
XAML version 2:
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Background="Red">
<ListView Background="Transparent" ItemsSource="{Binding ObservableResults}">
<ListView.ItemTemplate>
<DataTemplate>
<Expander>
<Expander.Header>
<TextBlock Text="{Binding Path=(ItemsControl.AlternationIndex), RelativeSource={RelativeSource AncestorType=ListViewItem}" />
</Expander.Header>
<ListView ItemsSource="{Binding}">
<ListView.View>
<GridView>
<GridViewColumn Header="Order ID" Width="650" DisplayMemberBinding="{Binding OrderId, UpdateSourceTrigger=PropertyChanged}" />
</GridView>
</ListView.View>
</ListView>
</Expander>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ScrollViewer>
But in this case no data are present in the view and the ReSharper says, that it:
Cannot resolve symbol 'OrderId'
Can anyone help me, what's wrong in here?

Cannot bind list of items in GridView column

I'm building an application that show to user the live result of a matches series. I setup the structure of data as follows: Countries->Leagues->Matches
In particular in the ViewModel I've created an observable collection of countries as follows:
private ObservableCollection<Models.Country> _countries = new ObservableCollection<Models.Country>();
public ObservableCollection<Models.Country> Country
{
get { return _countries; }
}
and the model:
public class Country
{
public string Name { get; set; }
public List<League> League { get; set; }
}
public class League
{
public string Name { get; set; }
public List<Event> Event { get; set; }
}
the class Event contains the properties of each event, in particular the name of the event, the date and so on..
I valorize this data as follows:
Country country = new Country();
country.Name = "Italy";
League league = new League();
league.Name = "Serie A";
League league2 = new League();
league2.Name = "Serie B";
Event #event = new Event();
#event.MatchHome = "Inter";
Event event2 = new Event();
#event.MatchHome = "Milan";
league.Event = new List<Event>();
league2.Event = new List<Event>();
league.Event.Add(#event);
league2.Event.Add(event2);
country.League = new List<League>();
country.League.Add(league);
country.League.Add(league2);
lsVm.Country.Add(country); //lsVm contains the ViewModel
How you can see I create an object called country (Italy) that will contains in this case two leagues (Serie A) and (Serie B). Each league contains one match actually in playing Serie A -> Inter and Serie B -> Milan
I add the league two the country, and finally the country to the observable collection in the viewmodel. Until here no problem. The problem's come in the xaml.
So I've organized all of this stuff inside of GroupViews, for doing this I'm using a CollectionViewSource, in particular:
<CollectionViewSource Source="{Binding Country}" x:Key="GroupedItems">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Name" />
<PropertyGroupDescription PropertyName="League.Name" />
</CollectionViewSource.GroupDescriptions>
the code above is located in my Window.Resources, and tell to CollectionViewSource to organize for country name and leagues name the respective leagues associated.
I've two ListView as this:
<ListView ItemsSource="{Binding Source={StaticResource GroupedItems}}" Name="Playing">
<ListView.View>
<GridView>
<GridViewColumn Header="Date" Width="150" DisplayMemberBinding="{Binding Path = League.Event.MatchDate}"/>
<GridViewColumn Header="Minutes" Width="70" DisplayMemberBinding="{Binding Path = League.Event.MatchMinute}"/>
<GridViewColumn Header="Home" Width="150" DisplayMemberBinding="{Binding Path = League.Event.MatchHome}"/>
<GridViewColumn Header="Score" Width="100" DisplayMemberBinding="{Binding Path = League.Event.MatchScore}"/>
<GridViewColumn Header="Away" Width="150" DisplayMemberBinding="{Binding Path = League.Event.MatchAway}"/>
</GridView>
</ListView.View>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Expander IsExpanded="True" Background="#4F4F4F" >
<Expander.Header>
<StackPanel Orientation="Horizontal" Height="22">
<TextBlock Text="{Binding Name}" FontWeight="Bold" Foreground="White" FontSize="22" VerticalAlignment="Bottom" />
<TextBlock Text="{Binding ItemCount}" FontSize="22" Foreground="Orange" FontWeight="Bold" FontStyle="Italic" Margin="10,0,0,0" VerticalAlignment="Bottom" />
<TextBlock Text=" Leagues" FontSize="22" Foreground="White" FontStyle="Italic" VerticalAlignment="Bottom" />
</StackPanel>
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
The GroupStyle contains the leagues that will contains each match, now the problem is that I can't see any league and any match 'cause this item are inside of a list. So for display them I should write in the xaml this code:
<PropertyGroupDescription PropertyName="League[0].Name" />
this fix the bug of the league name displayed into GroupStyle
and in the GridView:
<GridViewColumn Header="Casa" Width="150" DisplayMemberBinding="{Binding Path = League[0].Event[0].MatchHome}"/>
but this of course will display only the specific item.. not the list of items. I need help to fix this situation, I cannot figure out. Thanks.
If you want to use the ListView's grouping abilities, you have to provide it a flat list of the items you want to group (in your case, the leagues), not the header items. The CollectionView does the grouping for you by specifying GroupDescriptions.
For example, assuming the League class has a Country property:
class ViewModel
{
public ObservableCollection<Models.Country> Country { get; }
public IEnumerable<League> AllLeagues => Country.SelectMany(c => c.Leagues);
}
public class League
{
public string Name { get; set; }
public List<Event> Event { get; set; }
// add Country here
public Country Country { get; set; }
}
class
<CollectionViewSource Source="{Binding AllLeagues}" x:Key="GroupedItems">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Country" />
</CollectionViewSource.GroupDescriptions>
Then when you bind the columns, you bind directly to League properties, e.g.:
<GridViewColumn Header="Date" DisplayMemberBinding="{Binding Path=Event.MatchDate}"/>
And in the group style you can bind to Country properties, as you've done.
Alternative solution
If you want to display any hierarchical data in WPF you can either use a control that was built for it (such as the Xceed data grid) or hack it together with the built-in WPF data grid's row details.
Here's a sample XAML for this (note it uses your original data structures without the modifications I suggested above). These are essentially 3 data grids nested within each other. Each grid has its own set of columns, so you can define anything you want for each level (Country, League, Event).
<Window x:Class="WpfApp.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"
mc:Ignorable="d"
xmlns:app="clr-namespace:WpfApp"
d:DataContext="{d:DesignData ViewModel}">
<FrameworkElement.Resources>
<app:VisibilityToBooleanConverter x:Key="VisibilityToBooleanConverter" />
<DataTemplate x:Key="HeaderTemplate">
<Expander IsExpanded="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=DataGridRow}, Path=DetailsVisibility, Converter={StaticResource VisibilityToBooleanConverter}}" />
</DataTemplate>
<Style x:Key="DataGridStyle"
TargetType="DataGrid">
<Setter Property="RowHeaderTemplate"
Value="{StaticResource HeaderTemplate}" />
<Setter Property="RowDetailsVisibilityMode"
Value="Collapsed" />
<Setter Property="AutoGenerateColumns"
Value="False" />
<Setter Property="IsReadOnly"
Value="True" />
</Style>
</FrameworkElement.Resources>
<Grid>
<DataGrid ItemsSource="{Binding Country}"
Style="{StaticResource DataGridStyle}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Name}" />
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid ItemsSource="{Binding League}"
Style="{StaticResource DataGridStyle}">
<DataGrid.Columns>
<DataGridTextColumn Header="Name"
Binding="{Binding Name}" />
</DataGrid.Columns>
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<DataGrid ItemsSource="{Binding Event}"
AutoGenerateColumns="False"
IsReadOnly="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Match Home"
Binding="{Binding MatchHome}" />
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
</DataGrid>
</Grid>
</Window>
You'll also need the code for the converter I used:
public class VisibilityToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value as Visibility? == Visibility.Visible;
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> value as bool? == true ? Visibility.Visible : Visibility.Collapsed;
}

Create columns dynamically in a GridView in WPF MVVM

My MVVM WPF application currently has a GridView that binds to a ViewModel property and has the columns defined in the XAML:
<ListView Grid.Row="0" ItemsSource="{Binding GroupedOrders}">
<ListView.View>
<GridView>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Item2, Mode=OneWay}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Date" DisplayMemberBinding="{Binding Item1.Date, StringFormat={}{0:dd/MM/yyyy}}" />
<GridViewColumn Header="Name" DisplayMemberBinding="{Binding Item1.Name}" />
<!-- lots more -->
</GridView>
</ListView.View>
</ListView>
GroupedOrders is an ObservableCollection of a Tuple of two different item types: an "Order" object and a Boolean that controls whether or not, for this particular view, it is "selected".
However, that "selected" property hasn't been modelled well. It's come to light that each Order needs multiple "selected" properties, depending on the number of dates in the order, which can be between one and five.
To model this in the UI, I need to replace that single Checkbox GridViewColumn with a dynamic construct that creates a similar Checkbox GridviewColumn for each date in the order. So GroupedOrders becomes a Tuple <Order, List<bool>> instead, and there will need to be one column for each bool in the List.
At any given instance, the size of that list will be the same for all the Orders in the grid. But if the user loads new data into the grid, the size of the list will change.
However, I cannot see how to do this in MVVM. Existing solutions seem to be for code-behind where the GridView can be grabbed as an object and manipulated on the fly. The only MVVM solution I've seen to this is to use a Converter to building the entire GridView on the fly, which would seem to be massive overkill for this situation, where there are a lot of columns in the GridView but only a small number need to be dynamic.
Is there another way?
Not sure this is what you expect, but this is what I can think of.
View
<ListView ItemsSource="{Binding Orders}">
<ListView.View>
<GridView>
<GridViewColumn Header="State">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding States}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"></StackPanel>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.Resources>
<Style TargetType="GridViewColumnHeader">
<Setter Property="Visibility" Value="Collapsed" />
</Style>
</ListView.Resources>
<ListView.View>
<GridView>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox IsChecked="{Binding Value}" Content="{Binding Text}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="OrderNo" DisplayMemberBinding="{Binding OrderNo}" />
</GridView>
</ListView.View>
</ListView>
Code Behind
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Orders.Add(new Order { OrderNo = "Order001" });
Orders.Add(new Order { OrderNo = "Order002" });
Orders.Add(new Order { OrderNo = "Order003" });
}
private readonly ObservableCollection<Order> _orders = new ObservableCollection<Order>();
public ObservableCollection<Order> Orders
{
get { return _orders; }
}
}
public class Order
{
public Order()
{
States.Add(new State { Text = "Is Paid", Value = false });
States.Add(new State { Text = "Is Delivered", Value = false });
}
public string OrderNo { get; set; }
private readonly ObservableCollection<State> _states = new ObservableCollection<State>();
public ObservableCollection<State> States
{
get { return _states; }
}
}
public class State
{
public string Text { get; set; }
public bool Value { get; set; }
}
Result

Categories