Cannot bind list of items in GridView column - c#

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;
}

Related

Change a ListView's displayed columns based on a DependencyProperty

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.

How to bind a list of item in CollectionViewSource?

I'm stuck in this problem for days. Never found a solution. So I've a list of items called Country, in this list I've also another list of items called League. I've create a CollectionViewSource for organize all of this in nation name and league name but I can't bind all the leagues name:
<CollectionViewSource Source="{Binding Country}" x:Key="GroupedItems">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Name" />
<PropertyGroupDescription PropertyName="League.Name" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
I should write: <PropertyGroupDescription PropertyName="League[0].Name" />
but this only will bind the first item of the league list. Any solution?
PRACTICE EXAMPLE
suppose that there are two countries (Italy, England), Italy country have two leagues (Serie A and Serie B) and England have (Premiere League) only.
Serie A and Serie B contains in total 3 matches, 2 in Serie A and 1 in Serie B. In premiere League there are 4 matches.
In the ListView organization as my other topic that you can see here, in the UI should be displayed this structure:
ITALY
SERIE A
INTER
MILAN
SERIE B
JUVENTUS
ENGLAND
PREMIERE LEAGUE
LEICESTER
MANCHESTER UNITED
MANCHESTER CITY
LIVERPOOL
Now Italy and england are disposed into groupstyle as serie a, serie b and premiere league, with an expander (as you can see in my other question) there is the matches for each leagues.
The problem's that in the CollectionViewSource I cannot display the name of the league 'cause the leagues are inside a list, the same problem is for the matches, that is a list available in league, data structure:
Nations->Leagues->Matches
I went by the output you want. Please check and tell if this is what you want.
public partial class WinExpander : Window
{
public WinExpander()
{
InitializeComponent();
this.DataContext = new ViewModel();
}
}
public class ViewModel
{
public List<Country> Countries { get; set; }
public ViewModel()
{
Countries = new List<Country>();
}
}
public class Country
{
public string Name { get; set; }
public List<League> Leagues { get; set; }
public Country()
{
Leagues = new List<League>();
}
}
public class League
{
public string Name { get; set; }
public List<Match> Matches { get; set; }
public League()
{
Matches = new List<Match>();
}
}
public class Match
{
public string Name { get; set; }
}
XAML
<Window.Resources>
<CollectionViewSource x:Key="GroupedItems" Source="{Binding Countries}"/>
</Window.Resources>
...
<ItemsControl ItemsSource="{Binding Source={StaticResource GroupedItems}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Name}">
<ItemsControl ItemsSource="{Binding Leagues}" Margin="25 0 0 0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Expander Header="{Binding Name}">
<ItemsControl ItemsSource="{Binding Matches}" Margin="25 0 0 0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
You can replace any of the ItemsControl with ListBox too.
Another solution using GroupStyle and ListView according to user requirements.
<CollectionViewSource x:Key="GroupedItems" Source="{Binding Countries}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Name"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
...
<ListView ItemsSource="{Binding Source={StaticResource GroupedItems}}" Name="Playing">
<ListView.View>
<GridView>
<GridViewColumn Header="Country" Width="150" DisplayMemberBinding="{Binding Source={x:Static sys:String.Empty}}" />
<GridViewColumn Header="Leagues">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding Leagues}" Margin="25 0 0 0">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch" Background="#FFBCDAEC">
<TextBlock FontSize="18" Padding="5" Text="{Binding Name}"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Expander IsExpanded="True">
<Expander.Header>
<TextBlock Text="{Binding Name}" Foreground="Red" FontSize="22" VerticalAlignment="Bottom" />
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
</ListView>

Refresh DataGrid data on TreeViewItem selection change

I was able to implement Tree View using the link
Now I have attached the below Data Grid to it for displaying the city details like Area, Population, TimeZone etc.I was able to receive the event IsSelected upon selecting the City name from the treeview using example. But how do I bind the data of City model (Area, Population, TimeZone ) to a datagrid in the .xaml ? I tried using the CityViewModel directly but it never populates the data.CityViewModel has an observableCollection of "CityTown"( with props like Area, Population, TimeZone etc) property called "CityTowns" which I am populating when IsSelected is fired.My Tree View only has Region -> State -> City hierarchy. City towns should be displayed in grid not in tree.
//DemoWindow.xaml content:
<TabControl>
<TabItem Header="Load Towns">
<StackPanel Orientation="Horizontal">
<advanced: LoadOnDemandControl/>
<DataGrid ItemsSource="{Binding Path=local.CityViewModel.CityTowns}"
AutoGenerateColumns="False" IsReadOnly="True"
>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Popluation}" Header="Popluation"/>
<DataGridTextColumn Binding="{Binding Path=Revenue}" Header="Revenue"/>
<DataGridTextColumn Binding="{Binding Path=TimeZone}" Header="TimeZone"/>
<DataGridTextColumn Binding="{Binding Path=Area}" Header="Area"/>
</DataGrid.Columns>
</DataGrid>
</StackPanel>
</TabItem>
</TabControl>
//LoadOnDemandCcontrol.xaml:
<TreeView ItemsSource="{Binding Regions}">
<TreeView.ItemContainerStyle>
<!--
This Style binds a TreeViewItem to a TreeViewItemViewModel.
-->
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate
DataType="{x:Type local:RegionViewModel}"
ItemsSource="{Binding Children}"
>
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="Images\Region.png" />
<TextBlock Text="{Binding RegionName}" />
</StackPanel>
</HierarchicalDataTemplate>
<HierarchicalDataTemplate
DataType="{x:Type local:StateViewModel}"
ItemsSource="{Binding Children}"
>
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="Images\State.png" />
<TextBlock Text="{Binding StateName}" />
</StackPanel>
</HierarchicalDataTemplate>
<DataTemplate DataType="{x:Type local:CityViewModel}">
<StackPanel Orientation="Horizontal">
<Image Width="16" Height="16" Margin="3,0" Source="Images\City.png" />
<TextBlock Text="{Binding CityName}" />
</StackPanel>
</DataTemplate>
</TreeView.Resources>
</TreeView>
//CityTown.cs content:
public class CityTown
{
public int Area { get; set; }
public int Population { get; set; }
public string TimeZone { get; set; }
public int Revenue { get; set; }
public virtual City City { get; set; }
}
//CityViewModel.cs cocntent
public class CityViewModel : TreeViewItemViewModel
{
readonly City _city;
public CityViewModel(City city, StateViewModel parentState)
: base(parentState, false)
{
_city = city;
}
public string CityName
{
get { return _city.CityName; }
}
private ObservableCollection<CityTown> _CityTowns;
public ObservableCollection<CityTown> CityTowns
{
get { return Database.GetTowns(CityName); }
set { _CityTowns = value; }
}
}
//LoadOnDemandDemoControl.xaml.cs content:
public partial class LoadOnDemandDemoControl : UserControl
{
public LoadOnDemandDemoControl()
{
InitializeComponent();
Region[] regions = Database.GetRegions();
CountryViewModel viewModel = new CountryViewModel(regions);
base.DataContext = viewModel;
}
}
I solved it by defining the SelectedValuePath property for TreeView xaml element class and used the same in DataGrid element -> ItemsSoruce property -> Path -> SelectedItem.ViewModelDataCollection.

WPF - how to do binding the right way for a particular scenario?

I'm pretty new to WPF (moving from WinForms). I'm trying to transfer some scenario from a WinForms application to a WPF one:
A window has a ListView control with 3 columns.
There is a button there to add new rows to that ListView.
The first and the second columns contain the ComboBox control.
The third column must contain different controls but just one at a time is visible. Which one is visible, it depends on the selected value of the ComboBox at the first column.
The content of the ComboBox at the second column changes every time a user selects a value from the ComboBox at the first column.
The general scenario is: a user selects a type from the list of types from the first ComboBox, after that the second ComboBox changes its content to a list of supported operations for the selected type and the third column at that time must change its content to display a control that supports the input for that type.
I know how to implement it using WinForms but I have no idea yet how to do it using WPF. Can someone help me to implement it or can anyone help with the information that facilitate implementing that?
I have the code so far:
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
if (PropertyChanged != null) PropertyChanged(this, args);
}
}
public class RecordFilter : ViewModelBase
{
private static readonly ObservableCollection<KeyValuePair<PropertyInfo, string>> ColumnAliases =
new ObservableCollection<KeyValuePair<PropertyInfo, string>>(Card.ColumnAliases);
private KeyValuePair<PropertyInfo, string> _currentSelectedProperty;
public IEnumerable<OperationInfo> Operations
{
get
{
return Operations.GetOperationInfosForType(GetTypeUnwrapNullable(SelectedProperty.Key.PropertyType));
}
}
public OperationInfo SelectedOperation { get; set; }
public KeyValuePair<PropertyInfo, string> SelectedProperty
{
get { return _currentSelectedProperty; }
set
{
_currentSelectedProperty = value;
OnPropertyChanged("Operations");
}
}
public ObservableCollection<KeyValuePair<PropertyInfo, string>> Properties
{
get { return ColumnAliases; }
}
//DateTime or int or float, depends on the selected property type
//public object PropertyValue { get; set; }
}
Here is the XAML code:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Converters="clr-namespace:App.Converters" x:Class="App.DialogWindows.CardFilterWindow"
Title="Search filters" Height="347" Width="628" x:Name="wdw" ShowInTaskbar="False" WindowStartupLocation="CenterScreen">
<Window.Resources>
<Converters:NotNullObjectToEnabledConverter x:Key="NotNullObjectToEnabledConverter"/>
</Window.Resources>
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Center" Height="Auto">
<Button x:Name="bnOK" Margin="5" Width="41" Content="OK" IsDefault="True" Click="bnOK_Click"/>
<Button x:Name="bnCancel" Margin="5" Content="Отмена" IsCancel="True"/>
</StackPanel>
<ListView ItemsSource="{Binding Filters, ElementName=wdw}" Name="LvExpr" DataContext="{Binding Filters, ElementName=wdw}">
<ListView.Resources>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListView.Resources>
<ListView.View>
<GridView>
<GridViewColumn Header="Alias" Width="210">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox VerticalAlignment="Center"
ItemsSource="{Binding Properties}"
DisplayMemberPath="Value"
SelectedValue="{Binding SelectedProperty, Mode=TwoWay}"
/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Operation" Width="150">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ComboBox VerticalAlignment="Center"
ItemsSource="{Binding Operations}"
DisplayMemberPath="OperationAlias"
SelectedValue="{Binding SelectedOperation, Mode=TwoWay}"
/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Value" Width="100">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBox Text="ValidatesOnDataErrors=True}" />
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Width="33">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Button Tag="{Binding Mode=OneWay}" Click="BnDelete_Click" ToolTip="Delete filter">
<Image Source="delete.ico" Height="16" Width="16"/>
</Button>
</DataTemplate>
</GridViewColumn.CellTemplate>
<GridViewColumnHeader>
<DataGridCell>
<Button Click="ButtonAdd_Click" Height="22" Padding="0" ToolTip="Add filter">
<Image Source="plus.ico" Focusable="False"/>
</Button>
</DataGridCell>
</GridViewColumnHeader>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
</DockPanel>
</Window>
In your view-model, set up the list properties, and filter them out accordingly when the selected value changes (via the INotifyPropertyChanged.PropertyChanged event).
See this post for a comprehensive example. It uses a technique called MVVM that is used extensively with WPF and stands for ModelViewViewModel. I highly recommend you to learn this technique and utilize it in your XAML-related projects.
Here is one quick start tutorial, out of the many on the net.

WPF DataGrid different controls in the same column - incorrect binding

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>

Categories