I would like to add a footer to the ListView with the sum of two columns in his respective columns, Licenses and Scans, I found something similar (Here) but the footer is not being shown, if I add a 4 column into the GridViewthen the footer row is shown but without any values.The GridViewColumnHeadercontains a style which I would like to apply to the Footer as well (I have not pasted it to provide a Minimal, functional example)
This is the expected view:
xaml
<Window x:Class="WpfApp2.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:WpfApp2"
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:utils="Helper"
mc:Ignorable="d"
Title="MainWindow"
Height="450"
Width="800">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Window.Resources>
<CollectionViewSource x:Key="ViewSource"
Source="{Binding DataList}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="ToOrder" />
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
</Window.Resources>
<Grid>
<ListView ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ItemsSource="{Binding Source={StaticResource ViewSource}}">
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<DockPanel>
<Grid DockPanel.Dock="Bottom">
<Grid.Resources>
<local:SumConverter x:Key="sumConverter" />
</Grid.Resources>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding ElementName=Col1, Path=ActualWidth}"/>
<ColumnDefinition Width="{Binding ElementName=Col2, Path=ActualWidth}"/>
<ColumnDefinition Width="{Binding ElementName=Col3, Path=ActualWidth}"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="1"
Text="Sum: "
FontWeight="Bold" />
<TextBlock Grid.Column="1"
Grid.Row="1"
Text="{Binding Path=Items, Converter={StaticResource sumConverter}, ConverterParameter=1}"/>
<TextBlock Grid.Column="2"
Grid.Row="1"
Text="{Binding Path=Items, Converter={StaticResource sumConverter}, ConverterParameter=2}"/>
<Line Grid.Column="0"
Grid.Row="0"
Grid.ColumnSpan="3"
Stroke="Black"
X2="500"
Fill="Black"
VerticalAlignment="Center" />
</Grid>
<ItemsPresenter />
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
<ListView.View>
<GridView x:Name="OverviewGridView">
<GridViewColumn DisplayMemberBinding="{Binding Date, StringFormat=dd.MM.yyyy}"
Header="Date"
x:Name="Col1"/>
<GridViewColumn DisplayMemberBinding="{Binding LicenseCount}"
Header="Licenses"
x:Name="Col2"/>
<GridViewColumn DisplayMemberBinding="{Binding ScansCount}"
Header="Scans"
x:Name="Col3"/>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
ViewModel
namespace WpfApp2
{
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using WpfApp2.Annotations;
public class OverView
{
public int ToGroup { get; set; } = 1;
public DateTime Date { get; set; }
public int LicenseCount { get; set; }
public int ScansCount { get; set; }
}
public class ViewModel : INotifyPropertyChanged
{
private int _totalLicenses;
private ObservableCollection<OverView> _dataList;
private int _totalScans;
public ViewModel()
{
OverView a = new OverView
{
Date = DateTime.Now.Subtract(new TimeSpan(1, 0, 0, 0)),
LicenseCount = 2,
ScansCount = 7
};
OverView b = new OverView
{
Date = DateTime.Now.Subtract(new TimeSpan(5, 0, 0, 0)),
LicenseCount = 3,
ScansCount = 2
};
OverView c = new OverView { Date = DateTime.Now, LicenseCount = 5, ScansCount = 4 };
OverView d = new OverView
{
Date = DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0)),
LicenseCount = 1,
ScansCount = 3
};
DataList = new ObservableCollection<OverView> { a, b, c, d };
TotalLicenses = DataList.Sum(overview => overview.LicenseCount);
TotalScans = DataList.Sum(overview => overview.ScansCount);
}
public event PropertyChangedEventHandler PropertyChanged;
public ObservableCollection<OverView> DataList
{
get => _dataList;
set
{
_dataList = value;
OnPropertyChanged();
}
}
public int TotalLicenses
{
get => _totalLicenses;
set
{
_totalLicenses = value;
OnPropertyChanged();
}
}
public int TotalScans
{
get => _totalScans;
set
{
_totalScans = value;
OnPropertyChanged();
}
}
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
SumConverter
namespace WpfApp2
{
#region Using
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Windows.Data;
#endregion
public class SumConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
int sum = 0;
if (value is IEnumerable<object> total)
{
foreach (object o in total)
{
if (o is OverView overview)
{
int col = int.Parse((string)parameter);
sum += col == 1 ? overview.LicenseCount : overview.ScansCount;
}
}
return sum;
}
return 0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => 1;
}
}
Thanks in advance!
Edit, after further test, I had kind of success creating a new Property in the Overview object to group by it, then use a converter to sum the columns I need.
This is the result: The code is updated with the solution.
You cannot integrate it into the listview but you can put it below like this:
<StackPanel>
<GridViewRowPresenter DockPanel.Dock="Bottom"
Content="Sum"
Columns="{Binding ElementName=OverviewGridView, Path=Columns}">
</GridViewRowPresenter>
<ListView
HorizontalAlignment="Stretch"
ItemsSource="{Binding DataList}">
<ListView.View>
<GridView x:Name="OverviewGridView">
<GridViewColumn DisplayMemberBinding="{Binding Date}" Header="Date"/>
<GridViewColumn DisplayMemberBinding="{Binding LicenseCount}" Header="Licenses"/>
<GridViewColumn DisplayMemberBinding="{Binding ScansCount}" Header="Scans"/>
</GridView>
</ListView.View>
</ListView>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding ElementName=OverviewGridView, Path=Columns[0].ActualWidth}"/>
<ColumnDefinition Width="{Binding ElementName=OverviewGridView, Path=Columns[1].ActualWidth}"/>
<ColumnDefinition Width="{Binding ElementName=OverviewGridView, Path=Columns[2].ActualWidth}"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Summe" Grid.Column="0" Margin="10,0"/>
<TextBlock Text="{Binding DataList, FallbackValue=0,Converter={StaticResource Summierer}, ConverterParameter=1}" Margin="10,0" Grid.Column="1"/>
<TextBlock Text="{Binding DataList, FallbackValue=0,Converter={StaticResource Summierer}, ConverterParameter=2}" Margin="10,0" Grid.Column="2"/>
</Grid>
</StackPanel>
this is my converter (needs rewriting!! I didnt put any effort in it)
public class SummUp: IValueConverter
{
#region Convert
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var rtn = 0;
var coll = (value as ObservableCollection<OverView>);
var param = System.Convert.ToInt32(parameter);
foreach (var item in coll)
{
if (param == 1)
{
rtn += item.LicenseCount;
}
else if (param == 2)
{
rtn += item.ScansCount;
}
}
return rtn;
}//END Convert
#endregion Convert
#region ConvertBack
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}//END ConvertBack
#endregion ConvertBack
}//END class boolToNotVisibilityConverter : IValueConverter
Related
Summary
I'm trying to create an invoice. Each item on the invoice can have a sub collection of items. Implementing it was pretty standard, I created an item template to be used by the listbox and created text blocks to bind the listbox's itemsource's properties. Apart from text blocks, this template also houses another listbox containing the collection of sub items.
What I wanted to do:
I wanted to treat this main and sub listbox as one big listbox, at least in terms of the selecteditem. I want to cancel the currently selected row (either from main/sub) whenever I select a new row (still either from main/sub).
What I've tried:
First I've tried binding both the main/sub listbox's selected item to a single property but that was a failure. I think it has something to do with the main listbox's selected item getting triggered if I select an item on the sub listbox.
My second option was to create two properties with the same type to be used as the main and sub's selected item and placed an instruction to cancel out the each other's value if ever the new selected value is valid.
What's currently happening (this is the result of my 2nd try):
If I select a sub row it gets selected with no problems
If I select another sub row under the same parent as the previously selected row, it also works
The problem is that when I select a sub row from another parent after previously selecting a sub row. What happens is that the previous row's selected item status (at least visually) does not get de-selected (it's still highlighted as green as per my is selected trigger) yet the new row gets selected as well and shows the green highlight.
The Result
https://gfycat.com/fearlesssnivelinganemone
Code:
ShopModel
public class ShopModel
{
public class Sale : INotifyPropertyChangedClass
{
public Sale()
{
SaleItemMain = new ObservableCollection<SaleItem>();
}
private int id;
public int Id
{
get { return id; }
set { id = value; RaisePropertyChanged("Id"); }
}
private ObservableCollection<SaleItem> saleItemMain;
public ObservableCollection<SaleItem> SaleItemMain
{
get { return saleItemMain; }
set { saleItemMain = value; RaisePropertyChanged("SaleItemMain"); }
}
}
public class SaleItem : INotifyPropertyChangedClass
{
private int id;
private string name;
private decimal unitPrice;
private decimal quantity;
private ObservableCollection<SaleItem> saleItemSub;
public int Id
{
get { return id; }
set { id = value; RaisePropertyChanged("Id"); }
}
public string Name
{
get { return name; }
set { name = value; RaisePropertyChanged("Name"); }
}
public decimal Quantity
{
get { return quantity; }
set { quantity = value;
RaisePropertyChanged("Quantity");
RaisePropertyChanged("UnitPriceAmount");
}
}
public decimal UnitPrice
{
get { return unitPrice; }
set { unitPrice = value;
RaisePropertyChanged("UnitPrice");
RaisePropertyChanged("UnitPriceAmount");
}
}
public decimal UnitPriceAmount
{
get { return Quantity * UnitPrice; }
}
public ObservableCollection<SaleItem> SaleItemSub
{
get { return saleItemSub; }
set { saleItemSub = value; RaisePropertyChanged("SaleItemSub"); }
}
}
}
public class INotifyPropertyChangedClass : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
ShopViewModel
public class ShopViewModel : INotifyPropertyChangedClass
{
public ShopViewModel()
{
Sale = new Sale();
SaleItem mySaleItemSub1 = new SaleItem()
{
Id = 1,
Name = "Sub 1",
UnitPrice = 200,
Quantity = 5
};
SaleItem mySaleItemSub2 = new SaleItem()
{
Id = 1,
Name = "Sub 2",
UnitPrice = 200,
Quantity = 5
};
SaleItem mySaleItemSub3 = new SaleItem()
{
Id = 1,
Name = "Sub 3",
UnitPrice = 200,
Quantity = 5
};
SaleItem mySaleItemSub4 = new SaleItem()
{
Id = 1,
Name = "Sub 4",
UnitPrice = 200,
Quantity = 5
};
SaleItem mySaleItemMain1 = new SaleItem()
{
Id = 1,
Name = "Main 1",
UnitPrice = 200,
Quantity = 5,
SaleItemSub = new ObservableCollection<SaleItem>()
};
SaleItem mySaleItemMain2 = new SaleItem()
{
Id = 1,
Name = "Main 2",
UnitPrice = 200,
Quantity = 5,
SaleItemSub = new ObservableCollection<SaleItem>()
};
SaleItem mySaleItemMain3 = new SaleItem()
{
Id = 1,
Name = "Main 3",
UnitPrice = 200,
Quantity = 5,
SaleItemSub = new ObservableCollection<SaleItem>()
};
mySaleItemMain1.SaleItemSub.Add(mySaleItemSub1);
mySaleItemMain1.SaleItemSub.Add(mySaleItemSub2);
mySaleItemMain1.SaleItemSub.Add(mySaleItemSub3);
mySaleItemMain2.SaleItemSub.Add(mySaleItemSub4);
Sale.SaleItemMain.Add(mySaleItemMain1);
Sale.SaleItemMain.Add(mySaleItemMain2);
Sale.SaleItemMain.Add(mySaleItemMain3);
}
private Sale sale;
public Sale Sale
{
get { return sale; }
set { sale = value; RaisePropertyChanged("Sale"); }
}
private SaleItem selectedSaleItemMain;
public SaleItem SelectedSaleItemMain
{
get { return selectedSaleItemMain; }
set { selectedSaleItemMain = value;
RaisePropertyChanged("SelectedSaleItemMain");
if (SelectedSaleItemMain != null)
{
SelectedSaleItemSub = null;
RaisePropertyChanged("SelectedSaleItemSub");
}
}
}
private SaleItem selectedSaleItemSub;
public SaleItem SelectedSaleItemSub
{
get { return selectedSaleItemSub; }
set { selectedSaleItemSub = value;
RaisePropertyChanged("SelectedSaleItemSub");
if (SelectedSaleItemSub != null)
{
SelectedSaleItemMain = null;
RaisePropertyChanged("SelectedSaleItemMain");
}
}
}
}
ShopView
public partial class ShopView : UserControl
{
public ShopView()
{
DataContext = new ShopViewModel();
InitializeComponent();
}
}
Converter (My naming scheme goes as this: (property result)_(value sent)_(summary of what happens)
public class Visibility_SaleItemCollection_IfCountGreaterThan0VisibleElseCollapsed : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
ObservableCollection<SaleItem> saleItemCollection = value as ObservableCollection<SaleItem>;
if (saleItemCollection != null)
if (saleItemCollection.Count > 0)
return Visibility.Visible;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
XAML
<ListBox
SelectedItem="{Binding SelectedSaleItemMain,UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
ItemsSource="{Binding Sale.SaleItemMain,UpdateSourceTrigger=PropertyChanged}"
ItemContainerStyle="{DynamicResource InvoiceListBoxItemMain}"
VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
BorderThickness="0" Grid.Row="3"/>
(ListBox's ListBoxItem Style):
<Style x:Key="InvoiceListBoxItemMain" TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="backgroundBorder" Width="Auto" Background="White" >
<ContentPresenter Content="{TemplateBinding Content}">
<ContentPresenter.ContentTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="60" MaxWidth="60"/>
<ColumnDefinition Width="70" MaxWidth="70"/>
<ColumnDefinition Width="80" MaxWidth="80"/>
<ColumnDefinition Width="50" MaxWidth="50"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="1*" MinHeight="36"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="1"/>
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Margin="20,0,0,0" Foreground="{DynamicResource BlueGray.Dark.B}" Height="15"/>
<TextBlock Grid.Column="1" HorizontalAlignment="Right" TextWrapping="Wrap" Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Foreground="{DynamicResource BlueGray.Dark.B}"/>
<TextBlock Grid.Column="2" HorizontalAlignment="Right" TextWrapping="Wrap" Text="{Binding UnitPrice, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Foreground="{DynamicResource BlueGray.Dark.B}"/>
<TextBlock Grid.Column="3" HorizontalAlignment="Right" TextWrapping="Wrap" Text="{Binding UnitPriceAmount, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Foreground="{DynamicResource BlueGray.Dark.B}"/>
<Button local:Extensions.PathData="{DynamicResource TrashCan.1}" local:Extensions.PathWidth="8" Grid.Column="4" HorizontalAlignment="Center" Margin="0" VerticalAlignment="Center" Width="22" Style="{DynamicResource StandardButtonStyle}" Foreground="{DynamicResource Red.Default.B}" Height="22" FontSize="10"/>
<ListBox Visibility="{Binding Path=ItemSource,RelativeSource={RelativeSource AncestorType=ListBox},Converter={StaticResource Visibility_SaleItemCollection_IfCountGreaterThan0VisibleElseCollapsed}}"
SelectedItem="{Binding DataContext.SelectedSaleItemSub,RelativeSource={RelativeSource AncestorType=ListBox},UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
BorderThickness="0" Grid.ColumnSpan="5" Grid.Row="1"
ItemContainerStyle="{DynamicResource InvoiceListBoxItemSub}"
ItemsSource="{Binding SaleItemSub,UpdateSourceTrigger=PropertyChanged}"
VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" Margin="-1,0" />
<Rectangle Fill="{DynamicResource Gray.Default.B}" Grid.ColumnSpan="5" Grid.Row="2"/>
</Grid>
</DataTemplate>
</ContentPresenter.ContentTemplate>
</ContentPresenter>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="backgroundBorder" Property="Background" Value="{DynamicResource Green.Light.B}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="InvoiceListBoxItemSub" TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="backgroundBorder" Width="Auto" Background="{DynamicResource LightGray.Default.B}">
<ContentPresenter Content="{TemplateBinding Content}">
<ContentPresenter.ContentTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="60" MaxWidth="60"/>
<ColumnDefinition Width="70" MaxWidth="70"/>
<ColumnDefinition Width="80" MaxWidth="80"/>
<ColumnDefinition Width="50" MaxWidth="50"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MinHeight="36"/>
</Grid.RowDefinitions>
<TextBlock HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Margin="35,0,0,0" Foreground="{DynamicResource BlueGray.Dark.B}" Height="15"/>
<TextBlock Grid.Column="1" HorizontalAlignment="Right" TextWrapping="Wrap" Text="{Binding Quantity, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Foreground="{DynamicResource BlueGray.Dark.B}"/>
<TextBlock Grid.Column="2" HorizontalAlignment="Right" TextWrapping="Wrap" Text="{Binding UnitPrice, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Foreground="{DynamicResource BlueGray.Dark.B}"/>
<TextBlock Grid.Column="3" HorizontalAlignment="Right" TextWrapping="Wrap" Text="{Binding UnitPriceAmount, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Foreground="{DynamicResource BlueGray.Dark.B}"/>
<Button local:Extensions.PathData="{DynamicResource TrashCan.1}" local:Extensions.PathWidth="8" Grid.Column="4" HorizontalAlignment="Center" Margin="0" VerticalAlignment="Center" Width="22" Style="{DynamicResource StandardButtonStyle}" Foreground="{DynamicResource Red.Default.B}" Height="22" FontSize="10"/>
</Grid>
</DataTemplate>
</ContentPresenter.ContentTemplate>
</ContentPresenter>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="backgroundBorder" Property="Background" Value="{DynamicResource Green.Light.B}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I have a class that contains a subclass
public class CustomerDate
{
public string Date { set; get; }
public Customerdetails _Customerdetails { set; get; }
public CustomerDate()
{
_Customerdetails = new Customerdetails();
}
}
public class Customerdetails
{
public int Id { set; get; }
public string Name { set; get; }
public Customerdetails() { }
}
I Have a list of CustomerDate Objects that I want to bind to a listview grouped by subClass(Customerdetails).
My problem is that I can show SubClass property in listview's Gridview ({Binding Customerdetails.Name}) but I cannot show SubClass details in GroupStyle section.
<TextBlock Text="{Binding Path=Customerdetails.Name}" />
Not working But
<GridViewColumn DisplayMemberBinding="{Binding Customerdetails.Name}"
is working.
Any Idea?
List<CustomerDate> CustomerDateList = new List<CustomerDate>();
.
.
.
lv.ItemsSource = DBAccess.GetBadMonthlyPaymentCustomers();
CollectionView view =
(CollectionView)CollectionViewSource.GetDefaultView(lv.ItemsSource);
PropertyGroupDescription gd = new PropertyGroupDescription("Customerdetails");
view.GroupDescriptions.Add(gd);
<ListView x:Name="lv">
<ListView.View>
<GridView x:Name="GridView" >
<GridViewColumn DisplayMemberBinding="{Binding Date}" />
<GridViewColumn DisplayMemberBinding="{Binding Customerdetails.Name}" />
</GridView>
</ListView.View>
<ListView.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<StackPanel Margin="0,10,0,0" Orientation="Horizontal">
<TextBlock Text="{Binding Path=Customerdetails.Name}" />
</StackPanel>
<ItemsPresenter Grid.Row="1" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListView.GroupStyle>
</ListView>
The DataContext of a GroupItem is a CollectionViewGroup and not Customerdetails.
A group may contain several Customerdetails. You can bind to the Name property of any of them using the Items property of the CollectionViewGroup, e.g.:
<TextBlock Text="{Binding Items[0].Name}" />
This two solutions work for me thank to #mm8 :
<local:NameConverter x:Key="NameConverter"/>
<TextBox Text="{Binding Converter={StaticResource NameConverter}, Mode=OneWay}"
public class NameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return ((CustomerDate)((CollectionViewGroup)value).Items[0]).Customerdetails.Name;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
Items[0] is an Object of Class CustomerDate so its working this way too:
<TextBlock Text="{Binding Items[0].Customerdetails.Name}" />
For a future professional project, I need to evaluate the WPF capabilities.
In this context, I created a small test project, which contains 1 string tree, and 1 image grid. I want that my image grid shows all the jpeg images contained inside a given directory, and for each image, to show the extracted file name below the image, without its path and extension.
Actually, my demo works correctly according to my goal, except for one point: I added each formatted file name to show inside a List collection, which I tried to bind with a TextBlock shown on the bottom of each images. However this formatted name isn't visible, instead I see the complete file name, as if the TextBlock extracted it directly from the Image object.
I tried to resolve this issue by myself, following several tutorials, nothing worked for me. I cannot figure out what I'm doing wrong. Can someone explain to me?
Here is my xaml file content
<Window x:Class="VirtualTrees.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:VirtualTrees"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<Style x:Key="myHeaderStyle" TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="Visibility" Value="Collapsed" />
</Style>
<DataTemplate x:Key="itImageCell">
<WrapPanel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Image Width="120" Stretch="Uniform" Source="{Binding}"/>
<TextBlock Grid.Row="1" Width="120" Text="{Binding}" TextTrimming="CharacterEllipsis"/>
</Grid>
</WrapPanel>
</DataTemplate>
<local:ListToStringConverter x:Key="ListToStringConverter" />
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="400"/>
<ColumnDefinition Width="400*"/>
</Grid.ColumnDefinitions>
<ListView Margin="10" Name="lvStringTree">
<ListView.View>
<GridView>
<GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding Name}" />
<GridViewColumn Header="Age" Width="50" DisplayMemberBinding="{Binding Age}" />
<GridViewColumn Header="Mail" Width="150" DisplayMemberBinding="{Binding Mail}" />
</GridView>
</ListView.View>
</ListView>
<Grid x:Name="grImages" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<ListView Grid.Row="1" Name="lvImages" ItemsSource="{Binding Path=m_ImageList}" ItemTemplate="{StaticResource itImageCell}">
<ListView.Background>
<ImageBrush/>
</ListView.Background>
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="3" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
</ListView>
<TextBlock Name="tbImageName" Text="{Binding Path=m_ImageNames, Converter={StaticResource ResourceKey=ListToStringConverter}}" DataContext="{StaticResource itImageCell}" />
</Grid>
</Grid>
</Window>
And my c# code
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace VirtualTrees
{
[ValueConversion(typeof(List<string>), typeof(string))]
public class ListToStringConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (targetType != typeof(string))
throw new InvalidOperationException("The target must be a string");
return string.Join(", ", ((List<string>)value).ToArray());
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public class User
{
public string Name { get; set; }
public int Age { get; set; }
public string Mail { get; set; }
}
List<ImageSource> m_ImageList = new List<ImageSource>();
List<string> m_ImageNames = new List<string>();
string m_RegexPattern = #"\\([\w ]+).(?:jpg|png)$";
public MainWindow()
{
InitializeComponent();
PopulateStringTree();
PopulateImageGrid();
}
public void PopulateStringTree()
{
List<User> vstItems = new List<User>();
for (ulong i = 0; i < 100000; ++i)
{
vstItems.Add(new User() { Name = "John Doe", Age = 42, Mail = "john#doe-family.com" });
vstItems.Add(new User() { Name = "Jane Doe", Age = 39, Mail = "jane#doe-family.com" });
vstItems.Add(new User() { Name = "Sammy Doe", Age = 7, Mail = "sammy.doe#gmail.com" });
}
lvStringTree.ItemsSource = vstItems;
}
public void PopulateImageGrid()
{
// get jpeg image file list from target dir
string moviePosterPath = #"W:\Labo\WPF\VirtualTrees\VirtualTrees\Resources\Images";
List<string> fileNames = new List<string>(System.IO.Directory.EnumerateFiles(moviePosterPath, "*.jpg"));
// iterate through files
foreach (string fileName in fileNames)
{
// load image and add it to image list
m_ImageList.Add(new BitmapImage(new Uri(fileName)));
Console.WriteLine("filename " + fileName);
// extract image file name and add it to name list
Match regexMatch = Regex.Match(fileName.Trim(), m_RegexPattern);
m_ImageNames.Add(regexMatch.Groups[1].Value);
Console.WriteLine("Movie Name: " + regexMatch.Groups[1].Value);
}
// bind data to image grid
lvImages.ItemsSource = m_ImageList;
}
}
}
Your DataTemplate is the origin of the error. You have to check the binding of the TextBlock. You are binding to the DataContext which is a BitmapSource. The TextBlock implicitly calls BitmapSource.ToString() to get the string representation of the type. BitmapSource has ToString() overriden to return the full file path. To fix this, you need to use a IValueConverter.
Modified DataTemplate. The TextBlock binding now uses a converter to convert the BitmapSource to the filename:
<DataTemplate x:Key="itImageCell">
<WrapPanel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100" />
<RowDefinition Height="20" />
</Grid.RowDefinitions>
<Image Width="120"
Stretch="Uniform"
Source="{Binding}" />
<TextBlock Grid.Row="1"
Width="120"
Text="{Binding ., Converter={StaticResource BitmapSourceToFilenameConverter}}"
TextTrimming="CharacterEllipsis" />
</Grid>
</WrapPanel>
</DataTemplate>
The IValueConverter for the TextBlock binding to convert the BitmapSource to filename:
[ValueConversion(typeof(BitmapSource), typeof(string))]
public class BitmapSourceToFilenameConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is BitmapSource bitmapSource)
return bitmapSource.UriSource.AbsolutePath;
return Binding.DoNothing;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
A small mistake I realized in your code:
You are first setting a binding on the ListView:
<ListView Name="lvImages" ItemsSource="{Binding Path=m_ImageList}" />
and then you override (remove) it
// bind data to image grid
lvImages.ItemsSource = m_ImageList;
This is not a binding (the comment is not true).
You should make m_ImageList a ObservableCollection<ImageSource> instead of List. The ObservableCollection will automatically update the ListView when items are added, moved or removed. Then remove this line from your MainWindow class: lvImages.ItemsSource = m_ImageList;
I'm relatively new to WPF and MVVM, I have only written one other MVVM application. I would like to create my own calendar app but I'm having trouble setting the selected day.
XAML
<Grid Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="8*"/>
<ColumnDefinition Width="1*"/>
</Grid.ColumnDefinitions>
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="3" >
<TextBlock HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Text="<"></TextBlock>
</Button>
<Label HorizontalAlignment="Center" VerticalAlignment="Top" x:Name="CalendarTitle" Content="{Binding Path=DateTitle}" Grid.Column="1"/>
<Button HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="3" Grid.Column="2" Content=">"/>
</Grid>
<Grid Grid.Row="1">
<ItemsControl ItemsSource="{Binding Days}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="5" Columns="7" FirstColumn="3"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="1">
<Grid Background="Transparent" MouseDown="Grid_MouseDown" MouseLeave="Grid_MouseLeave" MouseEnter="Grid_MouseEnter">
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Label Content="{Binding DayNumber}"></Label>
<Label Content="{Binding Message}" VerticalAlignment="Center" HorizontalAlignment="Center"></Label>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Grid>
Using ItemsControl and Binding its ItemsSource to an ObservableCollection will work with bringing up data fine, how do I also properly create a SelectedDay attribute?
ViewModel
public class CalendarCellsViewModel : BaseINPC
{
public CalendarCellsViewModel()
{
DateTitle = $"{CultureInfo.CurrentCulture.DateTimeFormat.GetAbbreviatedMonthName(DateTime.Today.Month)} {DateTime.Today.Year}";
for(int i = 1; i <= 31; i++)
{
Days.Add(new CalendarDayData { DayNumber = i, Message = "Test" });
}
}
private string _dateTitle;
public string DateTitle
{
get { return _dateTitle; }
set { SetProperty(ref _dateTitle, value); }
}
private ObservableCollection<CalendarDayData> _days = new ObservableCollection<CalendarDayData>();
public ObservableCollection<CalendarDayData> Days
{
get { return _days; }
set { SetProperty(ref _days, value); }
}
}
Model
public class CalendarDayData : BaseINPC
{
public CalendarDayData()
{
}
public int DayNumber { get; set; }
public int ReminderStart { get; set; }
public int ReminderFrequency { get; set; }
public string Message { get; set; }
private bool _selectedDay = false;
public bool SelectedDay
{
get { return _selectedDay; }
set
{
if (_selectedDay != value)
{
_selectedDay = value;
SetProperty(ref _selectedDay, value);
}
}
}
}
Any tips or good resources I should take a look at?
ItemsControl doesn't support selection. You need to use ListBox for this. With that you can bind the SelectedItem property of your ListBox with SelectedDay property from your ViewModel.
XAML
<ListBox ItemsSource="{Binding Days}" SelectedItem="{Binding SelectedDay}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Rows="5" Columns="7" FirstColumn="3"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Border BorderBrush="Black" BorderThickness="1" HorizontalAlignment="Stretch">
<StackPanel Background="Transparent" HorizontalAlignment="Stretch">
<Label Content="{Binding DayNumber}"/>
<Label Content="{Binding Message}" VerticalAlignment="Center"
HorizontalAlignment="Center"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
You can get the SelectedItem by having a property to Bind in your ViewModel. Something like,
ViewModel
public class CalendarCellsViewModel : INotifyPropertyChanged
{
private string _dateTitle;
private ObservableCollection<CalendarDayData> _days;
private CalendarDayData _selectedDay;
public string DateTitle
{
get { return _dateTitle; }
set
{
_dateTitle = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DateTitle)));
}
}
public ObservableCollection<CalendarDayData> Days
{
get { return _days; }
set
{
_days = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Days)));
}
}
public CalendarDayData SelectedDay
{
get { return _selectedDay; }
set
{
_selectedDay = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedDay)));
}
}
public CalendarCellsViewModel()
{
DateTitle = $"{CultureInfo.CurrentCulture.DateTimeFormat.GetAbbreviatedMonthName(DateTime.Today.Month)} {DateTime.Today.Year}";
Days = new ObservableCollection<CalendarDayData>();
for (int i = 1; i <= 31; i++)
{
var day = new CalendarDayData { DayNumber = i, Message = "Test" };
if (i == DateTime.Today.Day)
SelectedDay = day;
Days.Add(day);
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Model
public class CalendarDayData
{
public int DayNumber { get; set; }
public int ReminderStart { get; set; }
public int ReminderFrequency { get; set; }
public string Message { get; set; }
}
Additional
If you want to differentiate your SelectedItem you can set below style to your Label.
<Label Content="{Binding DayNumber}">
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListBoxItem}}}"
Value="True">
<Setter Property="FontWeight" Value="ExtraBold"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
In my app I load image and verify I haven't too much blobs.
In additional, I put all images I loaded in list box and all image have a border.
Now, I want to change border color pet item according the Boolean value (true or false according numbers of blobs and their size).
If my image passed the border should be green, else red.
my relevent code:
public List<String> ImagePath = new List<String>();
public MainWindow()
{
InitializeComponent();
lb_Images.ItemsSource = ImagePath;
}
private void bu_addImage_Click(object sender, RoutedEventArgs e)
{
addImageToListBox();
}
private void addImageToListBox()
{
String imagePath = getImage();
// verfiy img haven't too much blobs
Boolean passed = imagePassed(imagePath);
// add the image to the list box
// I want to change border according passed value
// True - green; False- red.
ImagePath.Add(imagePath);
lb_Images.Items.Refresh();
}
XAML:
<Window x:Class="forQuestionWPF.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="216" Width="519">
<Window.Resources>
<DataTemplate x:Key="ImageGalleryDataTemplate">
<Grid>
<Border BorderBrush="Green" BorderThickness="2" Width="120" Height="120" Padding="5" Margin="5" CornerRadius="6">
<Image Source="{Binding}" Stretch="Fill" HorizontalAlignment="Center">
<Image.ToolTip>
<Grid>
<Image Source="{Binding}" Stretch="Fill" HorizontalAlignment="Center" Height="200" Width="200" />
</Grid>
</Image.ToolTip>
</Image>
</Border>
</Grid>
</DataTemplate>
<ItemsPanelTemplate x:Key="ImageGalleryItemsPanelTemplate">
<UniformGrid Rows="1" Columns="25" HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
</ItemsPanelTemplate>
</Window.Resources>
<Grid>
<Canvas Height="177" HorizontalAlignment="Left" Name="canvas1" VerticalAlignment="Top" Width="497">
<ListBox Canvas.Left="6" Canvas.Top="5" Height="166" Name="lb_Images" Width="441"
ItemTemplate="{StaticResource ImageGalleryDataTemplate}"
ItemsSource="{Binding Path=ImagePath}"
ItemsPanel="{StaticResource ImageGalleryItemsPanelTemplate}">
</ListBox>
<Button Canvas.Left="453" Canvas.Top="26" Content="Add" Height="64" Name="bu_addImage" Width="38" Click="bu_addImage_Click" />
</Canvas>
</Grid>
</Window>
For example, in the below image latest two items need to be with red border:
Thank you on any help!
AsfK
Instead of binding to imagePath (string) you can define a class:
public class ImageStuff {
public string ImagePath {get; set;}
public bool Passed {get; set;}
}
and add instance of this to the listbox.
Then, you can use a converter for your border like this:
<Border BorderThickness="2" Width="120" Height="120" Padding="5" Margin="5" CornerRadius="6" BorderBrush="{Binding Path=Passed, Mode=OneWay, Converter={StaticResource borderConverter}}">
where borderConverter is something like this:
public class borderConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value is bool)
{
if (!(bool)value == true)
return new SolidColorBrush(Colors.Red);
}
return new SolidColorBrush(Colors.Green);
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw NotImplementedException();
}
}
You have to include borderConverter in your resources like this:
<Window.Resources>
<src:borderConverter x:Key="borderConverter" />
</Window.Resources>
where src is the namespace.
If you want to change property of ImageStuff class dinamically you have to implement INotifyPropertyChanged so the binding will be updated.
You can do it without Converter, just add DataTrigger with Tag property to DataTemplate. This can affect to the performance because the Converter will work little much longer.
Add this class:
public class MyImage
{
public string ImagePath
{
get;
set;
}
public bool Flag
{
get;
set;
}
}
And in code-behind use like this:
public List<MyImage> ImagePath = new List<MyImage>();
Full example:
XAML
<DataTemplate x:Key="ImageGalleryDataTemplate">
<Grid>
<Border Name="MyBorder" BorderBrush="#FFFF9800" BorderThickness="1" Width="120" Height="120" Padding="5" Margin="5" CornerRadius="6">
<Image Name="MyImage" Tag="{Binding Path=Flag}" Source="{Binding Path=ImagePath}" Stretch="Fill" HorizontalAlignment="Center">
<Image.ToolTip>
<Grid>
<Image Source="{Binding Path=ImagePath}" Stretch="Fill" HorizontalAlignment="Center" Height="200" Width="200" />
</Grid>
</Image.ToolTip>
</Image>
</Border>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Path=Tag, ElementName=MyImage}" Value="True">
<Setter TargetName="MyBorder" Property="BorderBrush" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Tag, ElementName=MyImage}" Value="False">
<Setter TargetName="MyBorder" Property="BorderBrush" Value="Green" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Code-behind
public partial class MainWindow : Window
{
int imageNumber = 0;
public List<MyImage> ImagePath = new List<MyImage>();
public MainWindow()
{
InitializeComponent();
lb_Images.ItemsSource = ImagePath;
}
private void bu_addImage_Click(object sender, RoutedEventArgs e)
{
addImageToListBox();
}
private void addImageToListBox()
{
imageNumber++;
if (imageNumber == 4) imageNumber = 0;
string directoryPath = AppDomain.CurrentDomain.BaseDirectory;
// load input image
string ImageFilename = directoryPath + "img";
ImageFilename += imageNumber.ToString();
ImageFilename += ".jpg";
ImagePath.Add(new MyImage
{
ImagePath = ImageFilename,
Flag = false
});
lb_Images.Items.Refresh();
}
}
public class MyImage
{
public string ImagePath
{
get;
set;
}
public bool Flag
{
get;
set;
}
}
In order to the properties of MyImage class successfully changed, you need to implement the INotifyPropertyChanged interface.
You neeed to write BoolToColorConverter for this purpose. Find following converter code.
public sealed class BoolToColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool bValue = (bool)value;
if (bValue)
return Color.Green;
else
return Color.Red;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
Color color = (Color)value;
if (color == Color.Green)
return true;
else
return false;
}
}