I have a wpf control that can be used in two different windows. The control contains a ListView, which is fed by an ObservableCollection of the same class, regardless of which window is hosting the control.
In one window I want to show a certain set of columns, and in the other window a different set of columns.
I have included a trivial example of what I am trying to accomplish. For the purposes of this example, the xml is contained in a window rather than a UserControl.
Here is the xaml that defines the window and its two ListViews:
<Window x:Class="ListTest.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:ListTest"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Window.Resources>
<ControlTemplate x:Key="listOne" TargetType="{x:Type ListView}">
<ListView Margin="10,30,10,10" ItemsSource="{Binding MyList}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Food" Width="50" DisplayMemberBinding="{Binding Food}" />
</GridView>
</ListView.View>
</ListView>
</ControlTemplate>
<ControlTemplate x:Key="listTwo" TargetType="{x:Type ListView}">
<ListView Margin="10,30,10,10" ItemsSource="{Binding MyList}">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Number" Width="120" DisplayMemberBinding="{Binding Number}" />
<GridViewColumn Header="State" Width="50" DisplayMemberBinding="{Binding State}" />
</GridView>
</ListView.View>
</ListView>
</ControlTemplate>
</Window.Resources>
<Grid>
<CheckBox x:Name="checkBox" Content="Complex" HorizontalAlignment="Left"
Margin="10,10,10,10" VerticalAlignment="Top"
IsChecked="{Binding IsComplex}"/>
<ListView Margin="10" Name="lvUsers" Template="{StaticResource listTwo}" />
</Grid>
This is my trivial viewmodel, and the record class:
public class MyRecord
{
public MyRecord(string firstName, string food, int number, string state)
{
Name = firstName;
Food = food;
Number = number;
State = state;
}
public string Name { get; set; }
public string Food { get; set; }
public int Number { get; set; }
public string State { get; set; }
}
public class MainViewModel : ViewModelBase
{
private List<MyRecord> _recordList;
public MainViewModel()
{
_recordList = new List<MyRecord>();
_recordList = new List<MyRecord>();
_recordList.Add(new MyRecord("Lee", "pizza", 10, "ID"));
_recordList.Add(new MyRecord("Gary", "burger", 20, "UT"));
MyList = new ObservableCollection<MyRecord>(_recordList);
}
private ObservableCollection<MyRecord> _myList;
public ObservableCollection<MyRecord> MyList
{
get { return _myList; }
set
{
if (_myList != value)
{
_myList = value;
OnPropertyChanged(() => MyList);
}
}
}
private bool _isComplex = true;
public bool IsComplex
{
get { return _isComplex; }
set
{
if (_isComplex != value)
{
_isComplex = value;
OnPropertyChanged(() => IsComplex);
}
}
}
}
The next-to-last line of the xaml has a hard-coded Template assignment:
<ListView Margin="10" Name="lvUsers" Template="{StaticResource listTwo}" />
Changing that back and forth in the xaml results in the program displaying one ListView layout or the other without error.
I want to be able to set a property in the ViewModel that will control which layout is used - in this trivial case, I have a checkbox that should control the selected ListView.
I've tried triggers, which seems to be the simplest approach, but haven't found anything that makes the compiler happy.
Any suggestions would be appreciated!
Update:
Ed Plunkett's response showed me that I was making my question too hard. I didn't want to replace the whole ListView, just control what columns were displayed within it. Extracting a bit of his code results in exactly the behavior I wanted originally without going into the code-behind and replacing the entire ListView. The displayed columns in my sample now switch to the correct "view" when I toggle the checkbox. The code-behind is untouched, and the viewmodel remains the same. Thanks Ed! I've accepted his answer because it showed me the subset of code I needed, and I've changed the Title to reflect what the real question was.
This is the complete revised xaml:
<Window x:Class="AAWorkTest.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:ListTest"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainViewModel />
</Window.DataContext>
<Grid>
<CheckBox x:Name="checkBox" Content="Complex" HorizontalAlignment="Left"
Margin="10,10,10,10" VerticalAlignment="Top"
IsChecked="{Binding IsComplex}"/>
<ListView ItemsSource="{Binding MyList}" Margin="10,30,10,30" Name="lvUsers">
<ListView.Style>
<Style TargetType="ListView">
<Style.Triggers>
<DataTrigger Binding="{Binding IsComplex}" Value="False">
<Setter Property="View">
<Setter.Value>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Food" Width="50" DisplayMemberBinding="{Binding Food}" />
</GridView>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding IsComplex}" Value="True">
<Setter Property="View">
<Setter.Value>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Number" Width="120" DisplayMemberBinding="{Binding Number}" />
<GridViewColumn Header="State" Width="50" DisplayMemberBinding="{Binding State}" />
</GridView>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.Style>
</ListView>
</Grid>
You don't set properties on a control by replacing the template with one that creates a new, nested instance of the control with different properties. In WPF, a ControlTemplate determines how a control is displayed, it doesn't create the control. Instead, you set properties with a style that sets the properties. If it were a good idea to change the ListView's template, this is how you would do that.
Here's how you can do this (I don't recommend naming it UserControl1, of course):
UserControl1.xaml.cs
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public IEnumerable ItemsSource
{
get { return (IEnumerable)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static readonly DependencyProperty ItemsSourceProperty =
DependencyProperty.Register(nameof(ItemsSource), typeof(IEnumerable), typeof(UserControl1),
new PropertyMetadata(null));
public ViewPurpose ViewPurpose
{
get { return (ViewPurpose)GetValue(ViewPurposeProperty); }
set { SetValue(ViewPurposeProperty, value); }
}
public static readonly DependencyProperty ViewPurposeProperty =
DependencyProperty.Register(nameof(ViewPurpose), typeof(ViewPurpose), typeof(UserControl1),
new PropertyMetadata(ViewPurpose.None));
}
public enum ViewPurpose
{
None,
FoodPreference,
ContactInfo,
FredBarneyWilma
}
UserControl1.xaml
<UserControl
x:Class="WpfApp3.UserControl1"
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:WpfApp3"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<ListView
ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource AncestorType=UserControl}}"
>
<ListView.Style>
<Style TargetType="ListView">
<Style.Triggers>
<DataTrigger
Binding="{Binding ViewPurpose, RelativeSource={RelativeSource AncestorType=UserControl}}"
Value="FoodPreference"
>
<Setter Property="View">
<Setter.Value>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Food" Width="50" DisplayMemberBinding="{Binding Food}" />
</GridView>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger
Binding="{Binding ViewPurpose, RelativeSource={RelativeSource AncestorType=UserControl}}"
Value="ContactInfo"
>
<Setter Property="View">
<Setter.Value>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Number" Width="120" DisplayMemberBinding="{Binding Number}" />
<GridViewColumn Header="State" Width="50" DisplayMemberBinding="{Binding State}" />
</GridView>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ListView.Style>
</ListView>
</Grid>
</UserControl>
Usage example:
<StackPanel Orientation="Vertical">
<local:UserControl1
ViewPurpose="FoodPreference"
ItemsSource="{Binding SomeCollectionOfWhatever}"
/>
<local:UserControl1
ViewPurpose="ContactInfo"
ItemsSource="{Binding DifferentCollectionOfWhatever}"
/>
</StackPanel>
The enum is one option for specifying a set of columns. You could also give it a collection of column name, or a single string delimited by some special character ("Name|Food|Gas|Lodging") that would be split, and then do something in the UserControl to create the collection of columns based on that.
But if you have two or three predefined collections of columns, with custom widths and so on, this is quick and simple and does the job. You don't need to get too clever with this one.
Related
Good day,
I would like to ask question on how to properly bind the RelayCommand UpdateUserCommand() in toggle button. Please see my code below
<UserControl x:Class="istock.View.UserMasterList"
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:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:local="clr-namespace:istock.View"
mc:Ignorable="d"
MinHeight="450" Width="Auto" MinWidth="1024" FontFamily="Segoe UI LIght">
<Grid>
<Border Padding="30 15">
<StackPanel>
<Grid>
<TextBlock Text="User Management" FontSize="20" HorizontalAlignment="Left" Width="Auto" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Width="Auto">
<Button x:Name="btnNew" Foreground="#FFFF">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="PlusBoxOutline" Margin="0 5" />
<TextBlock Text="New User" Margin="5 3 5 0" />
</StackPanel>
</Button>
</StackPanel>
</Grid>
<ListView ItemsSource="{Binding Path = UserMasterListProp}">
<ListView.View>
<GridView>
<GridViewColumn Header="ID" Width="auto" DisplayMemberBinding="{Binding Path = Id}"></GridViewColumn>
<GridViewColumn Header="Username" Width="120" DisplayMemberBinding="{Binding Path = Username}"></GridViewColumn>
<GridViewColumn Header="Firstname" Width="120" DisplayMemberBinding="{Binding Path = Firstname}"></GridViewColumn>
<GridViewColumn Header="Middlename" Width="120" DisplayMemberBinding="{Binding Path = Middlename}"></GridViewColumn>
<GridViewColumn Header="Lastname" Width="120" DisplayMemberBinding="{Binding Path = Lastname}"></GridViewColumn>
<GridViewColumn Header="Role" Width="100" DisplayMemberBinding="{Binding Path = Role}"></GridViewColumn>
<GridViewColumn Header="Activated">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ToggleButton x:Name="tbtnActivated" Style="{DynamicResource MaterialDesignSwitchToggleButton}" IsChecked="{Binding Path = Activated}" Command="{Binding Path = UserViewModel.UpdateUserCommand}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Locked">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ToggleButton x:Name="tbtnLocked" Style="{DynamicResource MaterialDesignSwitchToggleButton}" IsChecked="{Binding Path = Locked}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</StackPanel>
</Border>
</Grid>
Here is my Command Code which was initialized and CanExecute() method is always true.
public class RelayCommand : ICommand
{
private Action ExecuteCmd;
public RelayCommand(Action paramAction)
{
this.ExecuteCmd = paramAction;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter)
{
return true;
}
public void Execute(object parameter)
{
this.ExecuteCmd();
}
}
And Here's my code in ViewModel which the UpdateUser() method will be fired
by my UpdateUserCommand()
#region Update Command and Method
private RelayCommand updateUserCommand;
public RelayCommand UpdateUserCommand
{
get { return updateUserCommand; }
}
public void UpdateUser()
{
try
{
var isExist = this.userService.ApplyService_FindByIdOrUsername(UserModel.Id, UserModel.Username);
if(isExist == null)
{
var isUpdated = this.userService.ApplyService_Update(UserModel);
if (isUpdated == true)
{
this.Message = "Update sucesss.";
this.Populate_UserMasterList();
}
}
else
{
this.Message = "Update failed. ID or Username already exist.";
return;
}
}
catch(Exception ex)
{
MessageBox.Show(ex.Message.ToString());
return;
}
}
#endregion
Here is the implementation of the Data Context in Code Behind of the User Control
public partial class UserMasterList : UserControl
{
public UserMasterList()
{
InitializeComponent();
UserViewModel userViewModel = new UserViewModel();
this.DataContext = userViewModel;
}
}
Please help. If ever my code is confusing, I am open for question. Thanks
You could bind to a property of the parent UserControl using a {RelativeSource}:
<ToggleButton x:Name="tbtnActivated"
Style="{DynamicResource MaterialDesignSwitchToggleButton}"
IsChecked="{Binding Activated}"
Command="{Binding DataContext.UpdateUserCommand,
RelativeSource={RelativeSource AncestorType=UserControl}}" />
I have a ListView (with an inner ListView) that displays data like this:
I would like to display the inner ListView headers above the grouping like so:
Is it possible to re-position the column headers as shown or simply create some fake headers on the outer ListView?
Here is the XAML code I have so far:
<ListView Name="ListView_GarnishmentCalculations"
ItemsSource="{Binding GarnishedEmployees, UpdateSourceTrigger=PropertyChanged}"
MaxHeight="{Binding ActualHeight,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ScrollContentPresenter}},
Converter={StaticResource MathConverter}, ConverterParameter=x-220}"
Margin="5,20,10,10"
ScrollViewer.CanContentScroll="True"
ScrollViewer.VerticalScrollBarVisibility="Auto"
Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="4">
<!-- Required for right justifying text in a TextBlock -->
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListView.ItemContainerStyle>
<!-- Group results and show EmpNo, Name and WorkState -->
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Grid ShowGridLines="False">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50" />
<ColumnDefinition Width="175" />
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Items[0].EmpNo}"
FontWeight="Bold"
Grid.Column="0" />
<TextBlock Text="{Binding Items[0].FullName}"
FontWeight="Bold"
Grid.Column="1" />
<TextBlock Text="{Binding Items[0].WorkState}"
FontWeight="Bold"
Grid.Column="2" />
</Grid>
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
<ListView.ItemTemplate>
<DataTemplate>
<!-- Inner ListView of garnishment details -->
<ListView ItemsSource="{Binding Garnishments}">
<ListView.View>
<GridView>
<!-- CaseID -->
<GridViewColumn Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding CaseNumber, Converter={StaticResource StringIsNullOrEmptyConverter}, ConverterParameter='No Case ID'}"
TextAlignment="Left">
</TextBlock>
</DataTemplate>
</GridViewColumn.CellTemplate>
<GridViewColumn.Header>
<GridViewColumnHeader Content=" Case ID" />
</GridViewColumn.Header>
</GridViewColumn>
<!-- Vendor -->
<GridViewColumn Width="150"
DisplayMemberBinding="{Binding Vendor}">
<GridViewColumn.Header>
<GridViewColumnHeader Content=" Vendor" />
</GridViewColumn.Header>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
This is what I came up with, NOT by my self I used my google fu skills for this.
Credit to this SO post.
So here is what I have for my model:
namespace Model
{
public class Case
{
public int CaseID { get; set; }
public int Vendor { get; set; }
}
}
And now user:
namespace Model
{
public class User
{
public int ID { get; set; }
public string Name { get; set; }
public string State { get; set; }
public List<Case> Cases { get; set; }
}
}
Now in my MainViewModel:
using Model;
using System.Collections.Generic;
namespace VM
{
public class MainViewModel : BaseViewModel
{
public MainViewModel()
{
Users = new List<User>();
for (int i = 0; i < 20000; i++)
{
Users.Add(new User
{
ID = i,
Name = $"John the {i + 1}",
State = i % 2 == 0 ? "CA" : "IL",
Cases = new List<Case>() { new Case { CaseID = (i + 1) * 10, Vendor = ((i + 1) * 10) - 2 }, new Case { CaseID = (i + 1) * 10, Vendor = ((i + 1) * 10) - 2 } }
});
}
}
private List<User> users;
public List<User> Users
{
get { return users; }
set { users = value; OnPropertyChanged(); }
}
}
}
On to the View:
<Window x:Class="SO_App.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:vm="clr-namespace:VM;assembly=VM"
xmlns:model="clr-namespace:Model;assembly=Model"
xmlns:local="clr-namespace:SO_App"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Window.Resources>
<CollectionViewSource Source="{Binding Users}" x:Key="Users"/>
</Window.Resources>
<Grid>
<ListView>
<ListView.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding Source={StaticResource Users}}"/>
</CompositeCollection>
</ListView.ItemsSource>
<ListView.View>
<GridView>
<GridViewColumn Header="Case ID" Width="100"/>
<GridViewColumn Header="Vendor" Width="100"/>
</GridView>
</ListView.View>
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<ContentPresenter/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<DataTemplate.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontWeight" Value="Bold"/>
</Style>
</DataTemplate.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text="{Binding ID}" MinWidth="50"/>
<TextBlock Text="{Binding Name}" MinWidth="250" Grid.Column="1"/>
<TextBlock Text="{Binding State}" MinWidth="50" Grid.Column="2"/>
<ListView Grid.Row="1" ItemsSource="{Binding Cases}" Grid.ColumnSpan="3">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding CaseID}" MinWidth="100"/>
<TextBlock Text="{Binding Vendor}" MinWidth="100" Grid.Column="1"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
Caveat:
You will need to handle the scroll event on the inner ListView so it doesn't swallow the mouse scroll.
P.S.
this is the BaseViewModel implementation:
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace VM
{
public class BaseViewModel : INotifyPropertyChanged
{
#region INPC
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string prop = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
}
#endregion
}
}
Which then produces this as a result:
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;
}
I am developing the application which performs checks on the list of items. Each item has the list of the checks that need to be performed on it. Each check can be one of 3 types: CheckBox, ComboBox, TextBox.
I would like to have Datagrid with 2 columns (one for item name, second for list of checks). Second column contains another DataGrid with 2 columns (one for check name, second for check control). The purpose is to have different types of controls in the same column bound with the Check models.
The problem is that binding with CheckValue doesn't work, however bindings with all the other properties work fine.
The last column contains CheckBoxes, TextBox and ComboBox, however they are not filled with any values.
Does anyone know what is wrong with below code?
Here are examples of model classes
public class Item
{
public string ItemName { get; set; }
public ObservableCollection<Check> Checks { get; set; }
public Item()
{
Checks = new ObservableCollection<Check>();
}
}
public enum CheckType
{
CheckBox,
ComboBox,
TextBox
}
public abstract class Check
{
public string CheckName { get; set; }
public CheckType CheckType { get; protected set; }
public abstract object CheckValue { get; set; }
}
public class CheckBox : Check
{
private bool checkValue;
public CheckBox()
{
CheckType = CheckType.CheckBox;
}
public override object CheckValue
{
get
{
return checkValue;
}
set
{
checkValue = (bool)value;
}
}
}
public class ComboBox : Check
{
private List<string> checkValue;
public ComboBox()
{
CheckType = CheckType.ComboBox;
}
public override object CheckValue
{
get
{
return checkValue;
}
set
{
checkValue = value as List<string>;
}
}
}
public class TextBox : Check
{
private string checkValue;
public TextBox()
{
CheckType = CheckType.TextBox;
}
public override object CheckValue
{
get
{
return checkValue;
}
set
{
checkValue = value as string;
}
}
}
public class MainViewModel
{
public ObservableCollection<Item> Items { get; set; }
public MainViewModel()
{
Items = new ObservableCollection<Item>();
Item item = new Item();
item.ItemName = "First item";
Check check1 = new CheckBox() { CheckName = "Check 1", CheckValue = true };
Check check2 = new CheckBox() { CheckName = "Check 2", CheckValue = false };
Check text1 = new TextBox() { CheckName = "Check 3", CheckValue = "Please enter check" };
Check combo1 = new ComboBox() { CheckName = "Check 4", CheckValue = new List<string> { "Value1", "Value2" } };
item.Checks.Add(check1);
item.Checks.Add(check2);
item.Checks.Add(text1);
item.Checks.Add(combo1);
Items.Add(item);
}
}
And finally here is XAML code of the main window.
<Window x:Class="ItemTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm ="clr-namespace:ItemTest"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<vm:MainViewModel x:Key="mainViewModel"/>
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource mainViewModel}}">
<DataGrid ItemsSource="{Binding Path=Items}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Item" Binding="{Binding ItemName}" />
<DataGridTemplateColumn Header="Checks">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<DataGrid ItemsSource="{Binding Checks}" AutoGenerateColumns="False" HeadersVisibility="None">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding CheckName}" />
<DataGridTemplateColumn>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding CheckType}" Value="CheckBox">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<CheckBox IsChecked="{Binding CheckValue}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding CheckType}" Value="ComboBox">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<ComboBox ItemsSource="{Binding CheckValue}" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding CheckType}" Value="TextBox">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBox Text="{Binding CheckValue}" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</Grid>
Just set the ItemControl's Content property:
<ContentControl Content="{Binding}">
WPF will automatically set DataTemplate's DataContext to its parent ContentControl's Content. But in your XAML you don't set the Content property (you only specify ContentControl's Style, but forget to set its Content).
And don't forget to set UpdateSourceTrigger=PropertyChanged on your control bindings, otherwise you may see no updates in your viewmodel.
XAML example working, with binding for BindingList :
<DataGrid x:Name="dataGridParametros"
Grid.Row="1"
Margin="5"
AutoGenerateColumns="False"
HeadersVisibility="All"
ItemsSource="{Binding}"
RowHeaderWidth="20"
SelectionUnit="FullRow"
ScrollViewer.CanContentScroll="True"
CanUserAddRows="false"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
FontFamily="Arial"
CellEditEnding="dataGridParametros_CellEditEnding" >
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding IdParametro}" Header="Id" FontFamily="Arial" IsReadOnly="True" Visibility="Hidden"/>
<DataGridTextColumn Binding="{Binding Codigo}" Header="Código" FontFamily="Arial" IsReadOnly="True"/>
<DataGridTextColumn Width="200" Binding="{Binding Mnemonico}" Header="Mnemonico" FontFamily="Arial" IsReadOnly="True" />
<DataGridTextColumn Width="250*" Binding="{Binding Descricao}" Header="Descrição" FontFamily="Arial" IsReadOnly="True" />
<DataGridTemplateColumn Header="Valor" Width="150">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding TipoCampo}" Value="CheckBox">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<CheckBox IsChecked="{Binding Valor , Mode=TwoWay , UpdateSourceTrigger=PropertyChanged}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding TipoCampo}" Value="ComboBox">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<ComboBox ItemsSource="{Binding Valor , Mode=TwoWay , UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding TipoCampo}" Value="TextBox">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<TextBox Text="{Binding Valor , Mode=TwoWay , UpdateSourceTrigger=PropertyChanged}" />
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
Let's say I have the following two classes in my model-folder:
public class Simple
{
public int Id { get; set; }
public string Display { get; set; }
public double Value1 { get; set; }
}
and
public class Extended : Simple
{
public double Value2 { get; set; }
public string Name { get; set; }
}
To display a collection of Simple I've created a UserControl which looks like:
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding}"
CanUserAddRows="False" CanUserDeleteRows="False">
<DataGrid.Resources>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Margin" Value="4,2"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTemplateColumn Header="Id" Width="Auto" SortMemberPath="Id">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Id}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Display" Width="*" SortMemberPath="Display">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Display}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Value1" Width="*" SortMemberPath="Value1">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Value1}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
The usage of this UserControl looks like:
<local:SEControl DataContext="{Binding Simples}"/>
Now I want to display a collection of Extended-Objects. My approach would now be to write another UserControl which just have two columns more than the other one.
My question now is: Is there a way to just write one UserControl which can handle Simple and Extended?
I also thought about a DataTemplate, but there I have to duplicate the logic too.
You can try to use Visibility.
Add the extended columns as here :
<DataGridTemplateColumn Visibility="{Binding IsExtended, Converter={StaticResource BoolToVis}, FallbackValue=Hidden}" Header="Test" Width="*" >
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding Test}"/>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
and using a converter you can switch whenever you need to show or not these columns.
So, when you change the ItemSource in order to contain Extended items, change also the IsExtended property to true and vice-versa.