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:
Related
I have a ListView where I trying to bind a list of lists.
I grouped the items of mainList based on Key. Below is a sample of XAML and
C# code for binding list to ListView.
I Group MainList by Key. it's OK and working but how can I group second ListView for sublist ?
this is XAML a listView for mainList and another listview in itemtemplate for sublist :
<ListView x:Name="MyList" >
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Key}" />
<ListView Grid.Row="1" ItemsSource="{Binding SubList}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding id}" />
<TextBlock Grid.Column="1" Text="{Binding name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Margin="10" Orientation="Horizontal">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
</Grid>
<TextBlock Grid.Column="0" Margin="10,0,0,0" Text="Key :" Style="{StaticResource SubHeaderTextBlockStyle}"/>
<TextBlock Grid.Column="1" Margin="10,0,10,0" Text="{Binding Key}" Style="{StaticResource TextBlockStyle}"/>
</StackPanel>
<ItemsPresenter Margin="10" Grid.Row="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
And this is c# :
public MainWindow()
{
InitializeComponent();
ObservableCollection<ListObj> MainList1 = new ObservableCollection<ListObj>
{
new ListObj
{
Key="key1", Value="value1",
SubList=new ObservableCollection<SubListObj>
{
new SubListObj{id="subid1", name="subname1" },
new SubListObj{id="subid2", name="subname2" }
}
},
new ListObj
{
Key="key2", Value="value2",
SubList=new ObservableCollection<SubListObj>
{
new SubListObj{id="subid3", name="subname3" }
}
}
};
MyList.ItemsSource = MainList1;
CollectionView view = (CollectionView)CollectionViewSource.GetDefaultView(MyList.ItemsSource);
PropertyGroupDescription gd = new PropertyGroupDescription("Key");
view.GroupDescriptions.Add(gd);
}
public class ListObj
{
public string Key { get; set; }
public string Value { get; set; }
public ObservableCollection<SubListObj> SubList { get; set; } = new ObservableCollection<SubListObj>();
}
public class SubListObj
{
public string id { get; set; }
public string name { get; set; }
}
You have to do it all in XAML (except the data initialization). Define two CollectionViewSource objects, one for the outer and one for the inner ListView.
To layout the group header use the GroupStyle.HeaderTemplate (instead of GroupStyle.ContainerStyle):
MainWindow.xaml
<Window>
<Window.Resources>
<CollectionViewSource x:Key="MainCollectionViewSource"
Source="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:SecondWindow}, Path=MainList}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="Key" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
<ListView ItemsSource="{Binding Source={StaticResource MainCollectionViewSource}}">
<ListView.ItemTemplate>
<DataTemplate DataType="ListObj">
<Grid>
<Grid.Resources>
<CollectionViewSource x:Key="SubCollectionViewSource"
Source="{Binding SubList}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="id" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="20" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="{Binding Value}" />
<ListView Grid.Row="1"
ItemsSource="{Binding Source={StaticResource SubCollectionViewSource}}">
<ListView.ItemTemplate>
<DataTemplate DataType="SubListObj">
<Grid>
<TextBlock Text="{Binding name}" />
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate DataType="GroupItem">
<TextBlock FontWeight="Bold"
FontSize="14"
Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate DataType="GroupItem">
<TextBlock FontWeight="Bold"
FontSize="14"
Text="{Binding Name}" />
</DataTemplate>
</GroupStyle.HeaderTemplate>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
public ObservableCollection<ListObj> MainList { get; set; }
public MainWindow()
{
InitializeComponent();
MainList1 = new ObservableCollection<ListObj>
{
new ListObj
{
Key="key1", Value="value1",
SubList=new ObservableCollection<SubListObj>
{
new SubListObj{id="subid1", name="subname1" },
new SubListObj{id="subid2", name="subname2" }
}
},
new ListObj
{
Key="key2", Value="value2",
SubList=new ObservableCollection<SubListObj>
{
new SubListObj{id="subid3", name="subname3" }
}
}
};
}
}
I'm grouping the items in listview as shown here. Now I want to bind some of my class properties in the expander header (group header), I'm not able to bind any properties from my class in the <ListView.GroupStyle> section but able to bind properties in <ListView.View>.
Code what I've tried:
<UserControl x:Class="ClientAccessTool.CasesAndTasksDetailsControl"
xmlns:core="clr-namespace:ClientAccessTool.Core;assembly=ClientAccessTool.Core"
DataContext="{x:Static core:SiteListViewModel.Instance}"
>
<UserControl.Resources>
<CollectionViewSource x:Key="groupByCases2" Source="{Binding SelectedSite.CasesAndTasksList}" >
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="CaseName" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</UserControl.Resources>
<Grid>
<ListView x:Name="lvCasesAndTasks"
ItemsSource="{Binding Source={StaticResource groupByCases2}}">
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Border Margin="3" Width="383" HorizontalAlignment="Left" >
<Expander x:Name="lvExpander" IsExpanded="True"
Style="{StaticResource ExpanderStyle3}" >
<Expander.Header>
<Grid VerticalAlignment="Center" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Viewbox Height="23" Margin="5" Grid.Column="0" DockPanel.Dock="Left">
<Image Source="/Images/RedFlag.png" RenderOptions.BitmapScalingMode="HighQuality"/>
</Viewbox>
<Grid Grid.Column="1" MinWidth="250" >
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="{Binding Name}"
FontSize="18" VerticalAlignment="Center"
FontWeight="SemiBold"
Foreground="Red"
Margin="5"
/>
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="5" >
<TextBlock Text="Number of tasks:" FontFamily="{StaticResource FontAwesome}" Foreground="Gray"/>
<TextBlock Margin="5 0 0 0"
Text="{Binding CountOfTasks}"
FontFamily="{StaticResource FontAwesome}" Foreground="Gray" >
</TextBlock>
</StackPanel>
</Grid>
</Grid>
</Expander.Header>
<ItemsPresenter />
</Expander>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
c# class:
namespace ClientAccessTool.Core
{
public class Case
{
public string SiteName { get; set; }
public string CaseName { get; set; }
public string CaseDescription { get; set; }
public List<Tasks> ListOfTasks { get; set; } = new List<Tasks>();
public string CountOfTasks => ListOfTasks.Count.ToString() ;
}
}
The item source SelectedSite.CasesAndTasksList is a List of type Case. I want to bind the CountOfTasks property inside the expander header but its not working.
I am having troubles grouping RadioButtons when using a WPF form and using DataBinding.
If I manually create my controls the behavior is as expected and the user is only allowed to select one item from the radiobutton group (bottom GroupBox), but if I use nested ItemControls to generate my layout and use data binding grouping does not work as expected and a user is allowed to select multiple radiobuttons in one group (top groupBox) as can be seen in this linked screenshot 1.
I have tried binding the GroupName property but fail to somehow find a binding property to use for GroupName property.
Perhaps the Text property of the TextBlock in the same grid row can be used, but I fail to find the proper Binding syntax to reference it.
What would be the way to get grouping to work?
Here is the XAML:
<Window x:Class="RadioButtonMadness.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" Title="MainWindow" SizeToContent="WidthAndHeight">
<Window.Resources>
<Style TargetType="RadioButton">
<Setter Property="Margin" Value="10" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Height" Value="22" />
</Style>
<Style TargetType="TextBlock">
<Setter Property="Margin" Value="10" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Height" Value="22" />
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions />
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<GroupBox Header="Prognostic factors - NOK">
<ItemsControl ItemsSource="{Binding PrognosticFactors}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Name="PrognosticFactors" Grid.Row="0" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions />
<TextBlock Text="{Binding Title}" />
<ItemsControl ItemsSource="{Binding Options}" Grid.Column="1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding Key}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</GroupBox>
<GroupBox Header="Prognostic factors - OK" Grid.Row="1">
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions/>
<TextBlock Text="Test4" Grid.Column="0"/>
<StackPanel Orientation="Horizontal" Grid.Column="1">
<RadioButton Content="One"/>
<RadioButton Content="Two"/>
<RadioButton Content="Three"/>
</StackPanel>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions />
<TextBlock Text="Test5" Grid.Column="0"/>
<StackPanel Orientation="Horizontal" Grid.Column="1">
<RadioButton Content="One"/>
<RadioButton Content="Two"/>
<RadioButton Content="Three"/>
</StackPanel>
</Grid>
</StackPanel>
</GroupBox>
</Grid>
</Window>
As well as the code behind:
using System;
using System.Collections.Generic;
using System.Windows;
namespace RadioButtonMadness
{
public class Option
{
public String Key{ get; set; }
public Double Value { get; set; }
public Option(String key, Double value)
{
Key = key;
Value = value;
}
}
public class PrognosticFactor
{
private String title;
public List<Option> Options
{
get
{
var list = new List<Option>();
list.Add(new Option("One", 1));
list.Add(new Option("Two", 2));
list.Add(new Option("Three", 3));
return list;
}
}
public String Title
{
get { return title; }
}
public PrognosticFactor(String value)
{
title = value;
}
}
public class ViewModel
{
public List<PrognosticFactor> PrognosticFactors
{
get
{
var list = new List<PrognosticFactor>();
list.Add(new PrognosticFactor("Test1"));
list.Add(new PrognosticFactor("Test2"));
list.Add(new PrognosticFactor("Test3"));
return list;
}
}
}
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel();
}
}
}
Just bind GroupName to the Title property. You can do it something like this:
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton Content="{Binding Key}" GroupName="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Grid}, Path=DataContext.Title}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
I have an ItemsControl to display translations text fields.
I want to setup validating, so if all translations are empty, there was an error, and fields was marked as "error".
Is there any possibilty to do this?
My xaml:
<ItemsControl x:Name="LanguageItemsControl" ItemsSource="{Binding Path=Translations, Mode=TwoWay}"
LostFocus="OnLostFocus" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="5,2,5,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="47*"/>
<ColumnDefinition Width="53*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" x:Name="ItemLabel" VerticalAlignment="Center"
Text="{Binding Path=Key, StringFormat={x:Static res:Resources.lblCaption}}" />
<TextBox Grid.Column="1" x:Name="ItemText" VerticalAlignment="Center"
HorizontalAlignment="Stretch" Margin="2,0,22,0"
Text="{Binding Path=Value, Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, NotifyOnValidationError=true}"
LostFocus="OnLostFocus"
AcceptsReturn="True"
MaxLines="2"
ScrollViewer.VerticalScrollBarVisibility="Auto"
MaxLength="150">
</TextBox>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
My model's class implements from IDataErrorInfo and INotifyPropertyChanged
Translations is an ObservableCollection of custom type "LanguageValue" with public properties Key and Value.
I had in my model string this[string columnName], which works perfect with simple text boxes (outside ItemsControl), but how can make this works with my items? I've thight something like:
public string this[string columnName]
{
get
{
string result = null;
...
if (columnName == "Translations" || columnName == "ItemText")
{
if (Translations.All(t => string.IsNullOrEmpty(t.Value)))
result = Properties.Resources.errMsgEnterName;
}
...
But of course this didn't work.
Any suggestions?
Sure, I'm giving you a full implementation but with just the "Value" property. Do the same with all other properties that you want to validate:
1.Translation Model with IDataErrorInfo interface implementation:
public class Translation : BindableBase, IDataErrorInfo
{
public string Value { get; set; }
public string this[string propertyName]
{
get
{
return GetErrorForPropery(propertyName);
}
}
public string Error { get; }
private string GetErrorForPropery(string propertyName)
{
switch (propertyName)
{
case "Value":
if (string.IsNullOrEmpty(Value))
{
return "Please enter value";
}
return string.Empty;
default:
return string.Empty;
}
}
}
2.Initialize Translations in your ViewModel:
public ObservableCollection<Translation> Translations { get; set; }
public MainViewModel()
{
Translations = new ObservableCollection<Translation>
{
new Translation {Value = "A"},
new Translation (),
new Translation {Value = "C"}
};
}
3.Xaml with ValidatesOnDataErrors on the Value TextBox:
<ItemsControl x:Name="LanguageItemsControl" ItemsSource="{Binding Path=Translations, Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="5,2,5,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="47*"/>
<ColumnDefinition Width="53*"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" x:Name="ItemLabel" VerticalAlignment="Center" Text="{Binding Path=Value, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
4.That will show a red box around the empty TextBox, if you want to display the error messeage when hovering overthe TextBox you need a tool tip:
<Window x:Class="WpfApplication1.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:customControlLib="clr-namespace:CustomControlLib;assembly=CustomControlLib"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
mc:Ignorable="d"
Title="MainWindow"
DataContext="{StaticResource MainViewModel}">
<Window.Resources>
<Style x:Key="TextBoxStyle" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<ItemsControl x:Name="LanguageItemsControl" ItemsSource="{Binding Path=Translations, Mode=TwoWay}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="5,2,5,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="47*"/>
<ColumnDefinition Width="53*"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" x:Name="ItemLabel" Style="{StaticResource TextBoxStyle}" VerticalAlignment="Center" Text="{Binding Path=Value, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I have a TabViewModel wich contains a dependency property CurrentViewModel. The CurrentViewModel property is bound to a ContentControl in the view TabView.xaml. The TabViewModel also contains a command to change the CurrentViewModel to a ProductViewModel:
public class TabViewModel: BaseViewModel
{
public string TabName { get; set; }
//public List<BaseViewModel> ViewModels { get; set; }
private PageViewModel _currentViewModel;
public PageViewModel CurrentViewModel
{
get { return _currentViewModel; }
set
{
_currentViewModel = value;
OnPropertyChanged("CurrentViewModel");
}
}
public TabViewModel(string tabName, PageViewModel currentViewModel)
{
TabName = tabName;
CurrentViewModel = currentViewModel;
}
private ICommand _navigateToProductViewModelCommand;
public ICommand NavigateToProductViewModelCommand
{
get
{
if (_navigateToProductViewModelCommand == null)
{
_navigateToProductViewModelCommand = new DelegateCommand<Product>(
(p) =>
{
CurrentViewModel = new ProductViewModel();
});
}
return _navigateToProductViewModelCommand;
}
}
}
TabView.xaml
<UserControl x:Class="Monitoring_Tool.Views.TabView"
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"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ProgressBar Value="{Binding Path=CurrentViewModel.PageProgress}" Height="5" Grid.Row="0" Margin="0,0,0,10">
<ProgressBar.Style>
<Style TargetType="{x:Type ProgressBar}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ProgressBar">
<Border BorderThickness="0,0,0,0" Background="LightGray" CornerRadius="0" Padding="0">
<Grid x:Name="PART_Track">
<Rectangle x:Name="PART_Indicator" HorizontalAlignment="Left" Fill="#00B6FA" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ProgressBar.Style>
</ProgressBar>
<ContentControl Content="{Binding Path=CurrentViewModel}" Grid.Row="1" />
</Grid>
I instantiate the TabViewModel like this:
new TabViewModel("Producten", new ProductsViewModel())
The ProductsView.xaml is shown like it should be. In the ProductsView.xaml I call the command from the TabViewModel like this:
<DataGrid.InputBindings>
<MouseBinding
MouseAction="LeftDoubleClick"
Command="{Binding DataContext.NavigateToProductViewModelCommand, RelativeSource={RelativeSource AncestorType={x:Type views:TabView}}}"/>
</DataGrid.InputBindings>
When the datagrid is empty, the command is executed and ProductView.xaml appears like it should be. But when the datagrid is not empty somthing strange happens:
the command is executed, and when I debug I can see that the currentViewModel is changed to ProductViewModel. Then when OnPropertyChanged("CurrentViewModel") is called. There is a set call (value = null) to a depedency property (SelectedAssetCategory) on the ProductsViewModel, wich was replaced and doesn't exits anymore?!
When I put CurrentViewModel = null the same thing happens, I can only do CurrentViewModel = new ProductsViewModel. So I guess it's somthing with updating the UI?
In the App.xaml I defined the following recources:
<DataTemplate DataType="{x:Type viewmodels:TabViewModel}">
<views:TabView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:ProductsViewModel}">
<views:ProductsView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:ProductViewModel}">
<views:ProductView />
</DataTemplate>
The ProductsViewModel looks like this:
class ProductsViewModel: PageViewModel
{
private readonly MonitotingToolEntities _databaseEntities;
public ProductsViewModel()
{
_databaseEntities = new MonitotingToolEntities();
AssetCategories = new ObservableCollection<AssetCategory>(_databaseEntities.AssetCategory.ToList())
{
new AssetCategory() {AssetCategoryID = 0, AssetCategoryName = "Alles"}
};
Results = new ObservableCollection<Product>();
}
public ObservableCollection<AssetCategory> AssetCategories { get; set; }
private AssetCategory _selectedAssetCategory;
public AssetCategory SelectedAssetCategory
{
get { return _selectedAssetCategory; }
set
{
_selectedAssetCategory = value; //this one is called with value = null
OnPropertyChanged("SelectedAssetCategory");
Filter();
}
}
public ObservableCollection<Product> Results { get; set; }
public void Filter()
{
Results.Clear();
List<Product> products =
SelectedAssetCategory.AssetCategoryID == 0
? _databaseEntities.Product.ToList()
: SelectedAssetCategory.Product.ToList();
foreach (Product product in products)
{
Results.Add(product);
}
}
}
ProductsView.xaml:
<UserControl x:Class="Monitoring_Tool.Views.ProductsView"
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:views="clr-namespace:Monitoring_Tool.Views"
xmlns:componentModel="clr-namespace:System.ComponentModel;assembly=WindowsBase"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:viewModels="clr-namespace:Monitoring_Tool.ViewModels"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<CollectionViewSource x:Key="CvsAssetCategories" Source="{Binding Path= AssetCategories}" >
<CollectionViewSource.SortDescriptions>
<componentModel:SortDescription PropertyName="AssetCategoryID"/>
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<CollectionViewSource x:Key="CvsResults" Source="{Binding Path= Results}" >
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="AssetCategory.AssetCategoryName" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
<Style TargetType="Image" x:Key="ImageDisabledStyle">
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.5" />
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid Margin="0, 0, 0, 10" Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="3*"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Asset Categorie:" Grid.Column="0" VerticalAlignment="Center" Margin="0,0,10,0"/>
<ComboBox Grid.Column="1"
ItemsSource="{Binding Source={StaticResource CvsAssetCategories}}"
DisplayMemberPath="AssetCategoryName"
SelectedItem="{Binding SelectedAssetCategory}"
Margin="0,0,10,0"/>
<TextBlock Text="Zoeken:" Grid.Column="3" VerticalAlignment="Center" Margin="0,0,10,0"/>
<ComboBox Grid.Column="4"
SelectedItem="{Binding SelectedSearchField}"
Margin="0,0,10,0"/>
<TextBox Text="{Binding Path=SearchQuery, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Grid.Column="5" Margin="0,0,10,0">
<TextBox.InputBindings>
<KeyBinding Command="{Binding Path=SearchCommand}" CommandParameter="{Binding SearchQuery}" Key="Enter" />
</TextBox.InputBindings>
</TextBox>
<Button Grid.Column="6"
Command="{Binding SearchCommand}"
CommandParameter="{Binding SearchQuery}"
Padding="5,0,5,0" Margin="0,0,10,0" >
<Button.Content>
<Image Source="/Recourses/SearchIcon.png"
Stretch="None" VerticalAlignment="Top" Style="{Binding Source={StaticResource ImageDisabledStyle}}"/>
</Button.Content>
</Button>
<Button Grid.Column="7"
Command="{Binding CancelSearchCommand}"
IsEnabled="{Binding CancelSearchEnabled}"
Padding="5,0,5,0">
<Button.Content>
<Image Source="/Recourses/CancelSearchIcon.png"
Stretch="None" VerticalAlignment="Top" Style="{Binding Source={StaticResource ImageDisabledStyle}}"/>
</Button.Content>
</Button>
</Grid>
<DataGrid Name="DgProducts" AutoGenerateColumns="False"
RowHeaderWidth="0" Margin="0,0,0,10" Grid.Row="1" IsReadOnly="True"
SelectionMode="Single" CanUserReorderColumns="False"
EnableRowVirtualization="True" VirtualizingPanel.IsVirtualizingWhenGrouping="True"
ItemsSource="{Binding Source={StaticResource CvsResults}}" SelectedItem="{Binding SelectedProduct}">
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0"/>
</Style>
</DataGrid.CellStyle>
<DataGrid.InputBindings>
<MouseBinding
MouseAction="LeftDoubleClick"
Command="{Binding DataContext.NavigateToProductViewModelCommand, RelativeSource={RelativeSource AncestorType={x:Type views:TabView}}}"
/>
</DataGrid.InputBindings>
<DataGrid.Resources>
<Style TargetType="DataGridColumnHeader" x:Key="DgVerticalColumnHeader">
<Setter Property="LayoutTransform">
<Setter.Value>
<RotateTransform Angle="270" />
</Setter.Value>
</Setter>
</Style>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}"
Color="LightGray"/>
<SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey }"
Color="Black"/>
</DataGrid.Resources>
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="GroupItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupItem">
<StackPanel>
<TextBlock Text="{Binding Path=Name}" Background="DarkGray" Padding="2,0,0,0"/>
<ItemsPresenter/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Path=Manager.ManagerName}" Header="Manager" />
<DataGridTextColumn Binding="{Binding Path=ProductName}" Header="Product" />
<DataGridTextColumn Binding="{Binding Path=MonitoringBy}" Header="Monitoring door" />
<DataGridTextColumn Binding="{Binding Path=AumProduct}" Header="AUM Product (mln)" />
<DataGridTextColumn Binding="{Binding Path=AumProductDate, StringFormat='{}{0:dd-MM-yyyy}'}" Header="Datum AUM Product" />
<DataGridTextColumn Binding="{Binding Path=AumStrategy}" Header="AUM Strategie (mln)" />
<DataGridTextColumn Binding="{Binding Path=AumStrategyDate, StringFormat='{}{0:dd-MM-yyyy}'}" Header="Datum AUM Strategie" />
<DataGridTextColumn Binding="{Binding Path=Aum}" Header="AUM (mln)" />
<DataGridTextColumn Binding="{Binding Path=TotalExpenseRatio}" Header="TER (bp)" />
<DataGridTextColumn Binding="{Binding Path=Fee}" Header="Total Fee" />
</DataGrid>
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Path=Results.Count, StringFormat='{}{0} Producten'}" Grid.Column="0" Margin="0,0,0,0"/>
<Button Grid.Column="2"
Content="Toevoegen"
Padding="5,0,5,0" Margin="0,0,10,0"
Command="{Binding AddProductCommand}" />
<Button Grid.Column="3"
Content="Verwijderen"
Padding="5,0,5,0"
Command="{Binding Path=RemoveProductCommand}"
CommandParameter="{Binding Path=SelectedProduct}"/>
</Grid>
</Grid>
PageViewModel is an abstract class:
public abstract class PageViewModel: BaseViewModel
{
private int _pageProgress;
public int PageProgress
{
get { return _pageProgress; }
set
{
_pageProgress = value;
OnPropertyChanged("PageProgress");
}
}
}
It's kind of weird but it has something to do with the collectionviewsource (CvsAssetCategories) bound to the combobox. If I bind directly to the combobox without using the collectionviewsource the dependency property is not called. However, I like to use the collectionviewsource for the sortdescriptor. My solution now is a not null check on the setter from the dependency property, but I think its nasty way to go.