Change ListView CellTemplate based on state of item - c#

I have a ListView that is has an ObservableCollection as its ItemsSource, and it has several columns. One of these is a State column which, depending on the current state of the item, shows a different message. Currently this is implemented as a basic string, and while it works it is far from pretty or userfriendly. I want to be able to vary the sort of output to more properly suit the state of the item.
I did do some research and know that I need to use a CellTemplate to affect the display, but all the different sorts of templates simply overwhelm me to the point where I can't figure out where to go next.
My code (excluding lots of other listview fluff) is as follows:
<ListView Name="itemsListView" ItemsSource="{Binding Source={StaticResource listingDataView}}" IsSynchronizedWithCurrentItem="True">
...
<ListView.View>
<GridView AllowsColumnReorder="true" ColumnHeaderToolTip="Item Information">
...
<GridViewColumn DisplayMemberBinding="{Binding Path=StatusMessage}" Width="283" Header="Status" HeaderContainerStyle="{StaticResource GVHeaderLeftAlignedStyle}" />
</GridView>
</ListView.View>
</ListView>
Yes, the items have hardcoded 'Status Message' that get updated alongside other properties that are actually relevant for the code, causing ugly duplication elsewhere in my code. (And yes, I know this is far from pretty, but I want to improve this too.) That property would be called ItemState as I am not all that creative.
So, my question is: how can I vary this column to have the most suitable display for the given state? Textual descriptions will do for many states, but some are rather lengthy and might profit from a text with a progress bar besides it, and maybe some sort of time remaining. Another state would profit from having a clickable hyperlink. In other words, I think I need at least 3 different CellTemplates.
I realize it is a rather open-ended question that largely suffers from the design mistakes of someone (=me) who has rather little experience with WPF, but that is exactly why I'm hoping someone experienced can set me straight with some basic code before I make an even worse mess of things than I have already. :)

You can use triggers to change the content of the cell, e.g.
<GridViewColumn Header="Status">
<GridViewColumn.CellTemplate>
<DataTemplate>
<ContentControl>
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding StateItem.HasError}" Value="True">
<Setter Property="ContentTemplate">
<Setter.Value>
<!-- Possibly create another contentcontrol which differentiates between errors -->
<DataTemplate>
<TextBlock Text="{Binding StateItem.Error}"
Foreground="Red"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding StateItem.HasError}" Value="False">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Image Source="Images/Default.ico"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
Code gets a bit crazy that way though if you branch it further but it's a way to do it.
Edit: The setters should set the ContentTemplate instead of the Content, apparently otherwise no new controls may be created and only one row shows the proper content since the content can only have one parent.

Related

WPF Datagrid Grouping and totals [duplicate]

I'm implementing a WPF DataGrid that contains projects with many key figures. Projects are grouped by project categories.
For each category there should be:
a row that shows in each key figure column sum of all rows for the column.
a target row that is not part of the datasource grid in binded to. target row tells for every column what is target for the year (e.g. how much money there's to spend).
These rows should be always on top in each group (sorting filtering).
My 1st solution was to have this data in group header. This is not a good solution because group header does not support columns. i.e. it should be constructed by getting column widths.
That could be done but it gets complicated when users want to reorder and hide columns.
DataGrid is using CollectionViewSource so it's not populated with C# code. Basically i'm extending this example: http://msdn.microsoft.com/en-us/library/ff407126.aspx
Thanks & Best Regards - matti
I have a hacked-together DataGrid with group subtotal rows in one of my projects. We weren't concerned about some of the issues you bring up, such as hiding and sorting columns so I don't know for sure if it can be extended for that. I also realize there could be performance issues that may be a problem with large sets (my window is operating 32 separate DataGrids - ouch). But it's a different direction from other solutions I've seen, so I thought I'd throw it up here and see if it helps you out.
My solution consists of 2 major components:
1. The subtotal rows aren't rows in the main DataGrid, but are separate DataGrids. I have 2 extra grids in each group actually: 1 in the header that is only displayed when the group is collapsed, and one beneath the ItemsPresenter. The ItemsSource for the subtotal DataGrids comes from a Converter that takes the items in the group and returns an aggregate view model. The columns of the subtotal grids are exactly the same as the main grid (filled out in DataGrid_Loaded, though I'm sure it could be done in xaml too).
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander Background="Gray" HorizontalAlignment="Left" IsExpanded="True"
ScrollViewer.CanContentScroll="True">
<Expander.Header>
<DataGrid Name="HeaderGrid" ItemsSource="{Binding Path=., Converter={StaticResource SumConverter}}"
Loaded="DataGrid_Loaded" HeadersVisibility="Row"
Margin="25 0 0 0" PreviewMouseDown="HeaderGrid_PreviewMouseDown">
<DataGrid.Style>
<Style TargetType="DataGrid">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=Expander}, Path=IsExpanded}"
Value="True">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.Style>
</DataGrid>
</Expander.Header>
<StackPanel>
<ItemsPresenter/>
<DataGrid Name="FooterGrid" ItemsSource="{Binding ElementName=HeaderGrid, Path=ItemsSource, Mode=OneWay}"
Loaded="DataGrid_Loaded" HeadersVisibility="Row"
Margin="50 0 0 0">
<DataGrid.Style>
<Style TargetType="DataGrid">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=Expander}, Path=IsExpanded}"
Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid>
</StackPanel>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
2. Then the issue is how to get all DataGrids to behave as if they were a single grid. I've handled that by subclassing DataGridTextColumn (we only have text in this case, but other column types should work too) in a class called DataGridSharedSizeTextColumn that mimics the SharedSizeGroup behavior of the Grid class. It has a string dependency property with a group name and keeps track of all columns in the same group. When Width.DesiredValue changes in one column, I update the MinWidth in all the other columns and force an update with DataGridOwner.UpdateLayout(). This class is also covering column reordering and does a group-wide update whenever DisplayIndex changes. I would think this method would also work with any other column property as long as it has a setter.
There were other annoying things to work out with selection, copying, etc. But it turned out to be pretty easy to handle with MouseEntered and MouseLeave events and by using a custom Copy command.
One option could be to add the rows in data source with special values for Name and other fields that do not make sense and use DataTrigger to show them with special colors and maybe some other.
Filtering is done in C# anyway so it doesn't affect these rows.
Sorting is the only problem here. It would be so cool just to tell that some rows are always with order 0 and with order 1 in the group. But cause i dunno how to do it I gotta make custom sorting in C# for all the columns instead of just declaring the sorting:
<CollectionViewSource.SortDescriptions>
<!-- Requires 'xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"' declaration. -->
<scm:SortDescription PropertyName="ProjectName"/>
<scm:SortDescription PropertyName="Complete" />
<scm:SortDescription PropertyName="DueDate" />
</CollectionViewSource.SortDescriptions>
EDIT: On top of everything else it has a major drawback comparing to my 1st solution (sum info in group header) because when filtering changes I should update the sums to be calculated only for visible rows.
So this answer is a complete hack and lacks all the elegance and uses no nice features that WPF is suppose to have :(

ListView ItemsSource binding

I have a ListView with 3 columns:
<ListView ItemsSource="{Binding ParamName}" HorizontalAlignment="Left" Height="109" Margin="10,87,0,0" VerticalAlignment="Top" ScrollViewer.HorizontalScrollBarVisibility="Disabled" Width="281">
<ListView.View>
<GridView>
<GridView.ColumnHeaderContainerStyle>
<Style TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="IsEnabled" Value="False"/>
<Style.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="TextElement.Foreground" Value="Black"/>
</Trigger>
</Style.Triggers>
</Style>
</GridView.ColumnHeaderContainerStyle>
<GridViewColumn Header="Name" Width="60"/>
<GridViewColumn Header="Type" Width="60"/>
<GridViewColumn Header="Content" Width="156"/>
</GridView>
</ListView.View>
</ListView>
Currently, I have set ParamName as my ItemsSource, which will obviously not achieve the desired result this way.
In my ViewModel, I have ObservableCollections for each Column (ParamName for Name, ParamType for Type and ParamContent for Content). Those ObservableCollections are correctly filled and I am able to receive their data through the Binding, but I cannot fill the columns with their respective data.
I have thought of certain possible solutions, but none of them seem to work. What would be the best approach for this problem?
Here's how it looks like (left) and how it should look like (right):
Naming them after their Types might be a little bit confusing.
You have three Collections, but only one is binded. So all columns have same value.
Try to create one Collection containing all you need:
ObservableCollection<MyRow>
where MyRow is a Class or Struct with Properties you need.
If you already have these Collections - try to concatenate it to one major Collection and tell the GridColumns which Properties you wish to bind to each Column, but I'm not sure if it's possible with ObservableCollections - what if they have different length?
And you can still create your own Collection from these three - just parse it...

WPF DataGrid: IsVirtualizingWhenGrouping="True" not working

I have a DataGrid that has a CollectionViewSource bound to it's ItemSource Property:
<DataGrid Grid.Row="0" RowBackground="#10808080" AlternatingRowBackground="Transparent"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
ItemsSource="{Binding Source={StaticResource bookingsViewSource}}"
RowHeight="27"
VirtualizingPanel.IsVirtualizingWhenGrouping="True"
VirtualizingPanel.IsContainerVirtualizable="True"
VirtualizingPanel.ScrollUnit="Item"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding date, StringFormat=dd.MM.yyyy}" Header="date"/>
<DataGridTextColumn Binding="{Binding Path=customers.name}" Header="customer"/>
<DataGridTextColumn Binding="{Binding Path=customers.street}" Header="adress"/>
</DataGrid.Columns>
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Expander Header="{Binding Path=Name}" IsExpanded="True">
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
</DataGrid>
bookingsViewSource is defined as
<CollectionViewSource x:Key="bookingsViewSource"
d:DesignSource="{d:DesignInstance {x:Type Database:bookings}}">
<CollectionViewSource.GroupDescriptions>
<PropertyGroupDescription PropertyName="providerID"/>
</CollectionViewSource.GroupDescriptions>
</CollectionViewSource>
and get's filled in code behind section. Everything was doing fine fast and smooth without grouping. But when I added grouping <PropertyGroupDescription PropertyName="providerID"/> the DataGrid needs around one minute to load.
In .NET 4.5 there is a new Property called VirtualizingPanel.IsVirtualizingWhenGrouping and I already set this to true but loading time was not decreasing.
I can not figure out why. Any ideas?
From the book: MacDonald M. - Pro WPF 4.5 in C#
A number of factors can break UI virtualization, sometimes when you don’t expect it:
Putting your list control in a ScrollViewer: The ScrollViewer provides a window onto
its child content. The problem is that the child content is given unlimited “virtual”
space. In this virtual space, the ListBox renders itself at full size, with all of its child
items on display. As a side effect, each item gets its own memory-hogging
ListBoxItem object. This problem occurs any time you place a ListBox in a container
that doesn’t attempt to constrain its size; for example, the same problem crops up if
you pop it into a StackPanel instead of a Grid.
Changing the list’s control template and failing to use the ItemsPresenter: The
ItemsPresenter uses the ItemsPanelTemplate, which specifies the
VirtualizingStackPanel. If you break this relationship or if you change the
ItemsPanelTemplate yourself so it doesn’t use a VirtualizingStackPanel, you’ll lose
the virtualization feature.
Not using data binding: It should be obvious, but if you fill a list programmatically—
for example, by dynamically creating the ListBoxItem objects you need—no
virtualization will occur. Of course, you can consider using your own optimization
strategy, such as creating just those objects that you need and only creating them at
the time they’re needed. You’ll see this technique in action with a TreeView that uses
just-in-time node creation to fill a directory tree in Chapter 22.
If you have a large list, you need to avoid these practices to ensure good performance.
Also, in my case the issue was caused by MahApps.Metro styles.

How can I create a reusable CellTemplate for a DataGridTemplateColumn in a DataGrid? (XAML only)

I have been searching for what I was hoping should be a very simple solution to this problem, but although I've found quite a few other questions on stackoverflow that seem to ask something similar, with some suggested answers, I've been through them all, and the answers either don't work, or don't actually answer the question.
I have a WPF DataGrid with a few columns, which I have set up as DataGridTemplateColumns, like so:
<DataGridTemplateColumn Header="Price 1" Width="60">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Price1}" ContentTemplate="{StaticResource NumericCellDataTemplate}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Price 2" Width="60">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Price2}" ContentTemplate="{StaticResource NumericCellDataTemplate}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Price 3" Width="60">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding Price3}" ContentTemplate="{StaticResource NumericCellDataTemplate}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
I have defined a reusable DataTemplate called "NumericCellDataTemplate", which provides some common features to each of the columns, including number formatting, alignment, and background highlighting on value changes. Note this is all driven off some special properties I've defined in my ViewModel:
<DataTemplate x:Key="NumericCellDataTemplate">
<Border Name="cellBorder" BorderThickness="0" BorderBrush="Transparent">
<Border.Style>
<Style TargetType="{x:Type Border}">
<Style.Setters>
<Setter Property="Background" Value="Transparent" />
</Style.Setters>
<Style.Triggers>
<DataTrigger Binding="{Binding IsRecentlyChanged}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource IsRecentlyChangedEnterStoryboard}" />
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource IsRecentlyChangedExitStoryboard}" />
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding Value, StringFormat='#,##0.00'}" HorizontalAlignment="Right" />
</Border>
</DataTemplate>
I have set up storyboards for those background color animations, to be triggered when the values change. The viewModel is tracking this and setting/unsetting my IsRecentlyChanged property for each value individually:
<Storyboard x:Key="IsRecentlyChangedEnterStoryboard">
<ColorAnimation Duration="0:0:0.3" To="LightSteelBlue" FillBehavior="HoldEnd" Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)" />
</Storyboard>
<Storyboard x:Key="IsRecentlyChangedExitStoryboard">
<ColorAnimation Duration="0:0:0.3" To="Transparent" FillBehavior="HoldEnd" Storyboard.TargetProperty="(Background).(SolidColorBrush.Color)" />
</Storyboard>
Everything here seems to work fine. Note that one of the reasons I am using a custom DataGridTemplateColumn, and not just a simple DataGridTextColumn, is that my property I am binding to (such as Price1) is a complex object, and not just a simple value type or string to be displayed. It is actually an IPropertyModel, which is an interface I have defined with a few useful properties for the view layer to bind to:
public interface IPropertyModel : INotifyPropertyChanged
{
string Name { get; }
bool IsRecentlyChanged { get; }
}
public interface IPropertyModel<T> : IPropertyModel
{
T Value { get; set; }
}
So all I would like to do is find a way to simplify the definition of those DataGridTemplateColumns I mentioned above, to a simple one-line thing like this:
<DataGridTemplateColumn Header="Price1" Width="60" Binding="{Binding Price1}" ContentTemplate="{StaticResource NumericCellDataTemplate}" />
Of course the problem here is that the DataGridTemplateColumn does not have a Binding property the way the DataGridTextColumn does. I have tried many of the suggestions, such as using a Style on the DataGridCell to completely override its Template with a new ControlTemplate, and provide a custom ContentPresenter. I have also tried doing this with a DataGridTextColumn, in order to take advantage of the fact that it already has a Binding property we could use - but I can't see how to access it from within the Style or the ContentPresenter.
Some of the suggestions talk about using the RelativeAncestor or TemplateBinding to get back to the DataGridTemplateColumn, and then read an attached property from it, such as ap:MyAttachedProperties.Binding - but as some have noted, once you are in the DataGridCell template, you no longer have access to the original DataGridTemplateColumn, because it is not in the Visual/Logical tree I guess? So after much experimentation that also did not prove helpful.
Any other suggestions would be appreciated. I am happy to use the DataGridTextColumn, DataGridTemplateColumn, or something else if anyone has a better idea. In this example I only have 3 columns, and one reusable DataTemplate for all of them. But Imagine a more complex scenario, where we have 20+ columns, possibly binding to one of several different reusable DataTemplates to do different kinds of formatting and display logic - so this is going to get messy if every column needs 7+ lines of XAML code.
Thanks!

Create WPF Watermark in Pure XAML

This is for a TextBox control on a login screen, where the TextBox contains the username. I want the TextBox to perform in the following way:
When the content is empty the content should be set to "Username".
When the TextBox is clicked I want the content to be set to
"" i.e; nothing (unless the content has already been edited by the user).
This is a pretty standard feature nowadays, something like this wordpress login (at the top of page). coudn't think of a better example than this I'm afraid :)
So, anyway, I've already done this using a ViewModel and it works well, but I'd like to know if this can be done purely from the XAML end. No business logic is concerned so I think it would be better to do it without the VM.
Find the below samples help you to find your way.
http://bendewey.wordpress.com/2008/09/27/wpf-shadowed-textbox-watermark/
http://www.c-sharpcorner.com/uploadfile/rahul4_saxena/watermark-textbox-in-wpf/
http://www.codeproject.com/Articles/26977/A-WatermarkTextBox-in-3-lines-of-XAML
Pure XAML:
<Grid>
<TextBox Width="250" VerticalAlignment="Center" HorizontalAlignment="Left" x:Name="SearchTermTextBox" Margin="5"/>
<TextBlock IsHitTestVisible="False" Text="Enter Search Term Here" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="10,0,0,0" Foreground="DarkGray">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=SearchTermTextBox}" Value="">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
Taken from: https://stackoverflow.com/a/21672408/4423545
The Extended WPF Toolkit has a Watermark Textbox that will do just what you're asking in pure XAML. There are other libraries out there as well.
The good thing about using the Extended WPF Toolkit is you can pick it up on Nuget and install and install updates directly through Visual Studio.

Categories