Create columns dynamically in a GridView in WPF MVVM - c#

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

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.

C# WPF Loop & Delete Checked ListView Items

As someone who has recently switched from WinForms to WPF, I'm still struggling and going nuts trying to figure out a way to loop through and delete checked ListView items.
This method gives me error: "ListView does not contain a definition for CheckedItems..."
if (lvFilesList != null)
{
foreach (ListViewItem lvItem in lvFilesList.CheckedItems)
{
lvItem.Checked = False;
}
}
My XAML Code:
<ListView Height="400" Width="400"
Name="lvFilesList"
ItemsSource="{Binding}"
IsSynchronizedWithCurrentItem="True">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn>
<GridViewColumn.CellTemplate>
<DataTemplate>
<CheckBox Name="chk" IsChecked="{Binding IsChecked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"></CheckBox>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="ID" DisplayMemberBinding="{Binding ID}"/>
<GridViewColumn Header="File" DisplayMemberBinding="{Binding File}"/>
<GridViewColumn Header="Author" DisplayMemberBinding="{Binding Author}"/>
</GridView.Columns>
</GridView>
</ListView.View>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<TextBlock FontWeight="Bold" Text="Group"/>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
It looks odd that the ItemsSource of your ListView is directly bound to the current DataContext by
ItemsSource="{Binding}"
The DataContext would usually hold a view model object with a collection-type property like
public class Item
{
public bool IsChecked { get; set; }
// other properties like ID, File, Author
}
public class ViewModel
{
public ObservableCollection<Item> Items { get; }
= new ObservableCollection<Item>();
}
and the Binding would be
ItemsSource="{Binding Items}"
Then the view model could have a method that deletes all checked items, like
public void DeleteCheckedItems()
{
var checkedItems = Items.Where(item => item.IsChecked).ToList();
checkedItems.ForEach(item => Items.Remove(item));
}
Note that you usually assign an instance of the view model class to the DataContext of your main view, e.g. in the MainWindow constructor:
private readonly ViewModel viewModel = new ViewModel();
public MainWindow()
{
InitializeComponent();
DataContext = viewModel;
viewModel.Items.Add(new Item { ID = 1, ... });
viewModel.Items.Add(new Item { ID = 2, ... });
}

Binding ListView field to a nested list WPF

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

WPF ListView Column of Text Some Rows Underlined Some Not

I need to display databound data in a listview such that some of the cells of the same column are underlined while others are not:
How do I create a data template that will display different control types in the same cell? (Visual Studio 2013, .NET 4.5.2)
XAML:
<ListView Grid.Column="3" ItemsSource="{Binding Results.MyItemSource, Mode=OneWay}">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Width="Auto" Header=" " DisplayMemberBinding="{Binding Path=Caption, Mode=OneWay}"/>
<GridViewColumn Width="Auto" Header=" " DisplayMemberBinding="{Binding Path=Value, Mode=OneWay}" >
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
ItemSource:
public List<object> MyItemSource
{
get
{
return new List<object>
{
new CaptionValuePair {Caption = "Caption1", Value = new TextBlock {Text = "Value1"}},
new CaptionValuePair {Caption = "Caption2", Value = new TextBlock {Text = "Value2", TextDecorations = TextDecorations.Underline}}
};
}
}
Bound type:
public class CaptionValuePair
{
public string Caption { get; set; }
public object Value { get; set; }
}
Problem: The StackPanel is just displaying the typename of a textblock.
Instead of a StackPanel, using an ItemsControl and binding its ItemsSource property worked.
XAML:
<GridViewColumn.CellTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding Path=Value, Mode=OneWay}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
Bound type:
public List<UIElement> Value{ get; set; }

WPF Listview databinding to generic List, dynamically filled by WCF service

In WPF app I have a WCF service which dynamically fills a generic List object from a backend database.
How in this case (List created in runtime), I could bind List items to a ListView object items?
It is the Data contract for my Web service:
....
[DataContract]
public class MeetList
{
[DataMember]
public string MeetDate;
[DataMember]
public string MeetTime;
[DataMember]
public string MeetDescr;
.....
static internal List<MeetList> LoadMeetings(string dynamicsNavXml)
{
...// Loads XML stream into the WCF type
}
Here in this event handler I read the WCF service and Loop through a List object:
private void AllMeetings()
{
Customer_ServiceClient service = new Customer_ServiceClient();
foreach (MeetList meet in service.ReadMeetList())
{
?????? = meet.MeetDate; // it's here that I bumped into a problem
?????? = meet.MeetTime; //
?????? = meet.MeetDescr;//
}
}
My Listview XAML:
<Grid>
<ListView Height="100" Width="434" Margin="0,22,0,0" Name="lvItems" ItemsSource="{Binding}" IsSynchronizedWithCurrentItem="True" SelectionMode="Single">
<ListView.View>
<GridView>
<GridViewColumn Header="Date" Width="100" HeaderTemplate="{StaticResource DateHeader}" CellTemplate="{DynamicResource DateCell}"/>
<GridViewColumn Header="Time" Width="100" HeaderTemplate="{StaticResource TimeHeader}" CellTemplate="{DynamicResource TimeCell}"/>
<GridViewColumn Header="Description" Width="200" HeaderTemplate="{StaticResource DescriptionHeader}" CellTemplate="{DynamicResource DescriptionCell}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
And data templates for this ListView:
<Window.Resources>
<DataTemplate x:Key="DateHeader">
<StackPanel Orientation="Horizontal">
<TextBlock Margin="10,0,0,0" Text="Date" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
<DataTemplate x:Key="DateCell" DataType="Profile">
<StackPanel Orientation="Horizontal">
<TextBlock>
<TextBlock.Text>
<Binding Path="MeetDate" />
</TextBlock.Text>
</TextBlock>
</StackPanel>
</DataTemplate>
......
How in this case (List created in runtime), I could bind my generic List items to a ListView object items?
I tried to use lvItems.ItemsSource = profiles; , but it doesn't work in my event handler
List doesn't have behaviour to notify that items count is changed. You should use a list with support INotifyCollectionChanged.. for example: ObservableCollection<T>. ObservableCollection<T> will inform your lvItems that items count is changed and it will be properly display.
Using intermediate ObservableCollection:
ObservableCollection<Meets> _MeetCollection =
new ObservableCollection<Meets>();
public ObservableCollection<Meets> MeetCollection
{ get { return _MeetCollection; } }
public class Meets
{
public string Date { get; set; }
public string Time { get; set; }
public string Description { get; set; }
}
In the event handler loop we put:
private void AllMeetings()
{
Customer_ServiceClient service = new Customer_ServiceClient();
_MeetCollection.Clear();
foreach (MeetList meet in service.ReadMeetList())
{
_MeetCollection.Add(new Meets
{
Date = meet.MeetDate,
Time = meet.MeetTime,
Description = meet.MeetDescr
});
}
}
And XAML binding is changed to:
<Grid>
<ListView Height="100" Width="434" Margin="0,22,0,0" Name="lvItems" ItemsSource="{Binding MeetCollection}">
<ListView.View>
<GridView>
<GridViewColumn Header="Date" Width="100" DisplayMemberBinding="{Binding Date}"/>
<GridViewColumn Header="Time" Width="100" DisplayMemberBinding="{Binding Time}"/>
<GridViewColumn Header="Description" Width="200" DisplayMemberBinding="{Binding Description}"/>
</GridView>
</ListView.View>
</ListView>
</Grid>

Categories