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 :(
Related
I am using the DataGridControl from the xceeddatagrid library(http://schemas.xceed.com/wpf/xaml/datagrid). What I am trying to do is hide/Collapse the datarow based on a property from the class model. I am trying to use a datatrigger. My issue is that the DataGrid will remove the data from the datarow but not the physical space of the row. So it leaves a blank row. I tried a regular out of the box .net datagrid and I have no issue. I stripped away all styles and still have this issue.
xmlns:xcdg="http://schemas.xceed.com/wpf/xaml/datagrid"
<xcdg:DataGridControl x:Name="dgr"
ItemsSource="{Binding Path=.Data}"
AutoCreateColumns="False">
<xcdg:DataGridControl.Resources>
<Style TargetType="{x:Type xcdg:DataRow}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=.IsTrue}" Value="true">
<Setter Property="Visibility" Value="Collapsed" />
<Setter Property="Visibility" Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</xcdg:DataGridControl.Resources>
<xcdg:DataGridControl.Columns>
<xcdg:Column FieldName="IsToday" Title="Today" Width="40" />
<xcdg:Column FieldName="Name" Title="Name" Width="70" />
<xcdg:Column FieldName="Address" Title="Address" Width="40" />
</xcdg:DataGridControl.Columns>
</xcdg:DataGridControl>
Check out this doc page about the Visibility Enum. It would seem the value you need is Collapsed:
Do not display the element, and do not reserve space for it in layout.
I'm not sure that you need the "." in the Path of the DataTrigger Binding. I believe that each DataRow's IsTrue property will be accessed implicitly without the dot.
Besides that, the main issue I see is that there are two setters for the same property within the DataTrigger. Based on your description, it would seem that the DataRow's Visibility is being set to Collapsed, but then to Hidden, which does exactly what you describe as the problem:
Do not display the element, but reserve space for the element in layout.
So, I believe the solution is to remove the conflicting Visiblity Setter.
I have an almost default WPF ComboBox:
What is the simplest way to align items, for instance, to the right in combobox's input and/or drop down?
I've looked around and found only solutions messing with the control templates which in my opinion is too long shot for such a simple thing. I can't believe there is no simpler solution we can do for aligning the items.
UPDATE:
I've slightly reformulated the question to cover a broader set of cases further readers might experience on this topic as it turned out thanks to dkozl's answer.
Also, it should prevent some people from trying to close this question as a duplicate.
If you want to right align both selected value and drop down items then setting HorizontalContentAlignment="Right" against ComboBox should be enough
<ComboBox ... HorizontalContentAlignment="Right">
if you want to right align only drop down items then you need to change HorizontalContentAlignment of the ComboBoxItem
<ComboBox>
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Right"/>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
and to right align only selected value combination of both
<ComboBox ... HorizontalContentAlignment="Right">
<ComboBox.ItemContainerStyle>
<Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
</Style>
</ComboBox.ItemContainerStyle>
</ComboBox>
I've tried both things from dkozl's answer and by some reason setting HorizontalContentAlignment against my ComboBox was sufficient to align to the right items in both combobox's input and drop down.
I had the problem of aligning the selected item on the right, so that users can always see the end of the string.
Simplest solution I found was the following
<ComboBox ...>
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock HorizontalAlignment="Right" Text="{Binding textProp}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
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.
I've got a ComboBox I'm hiding and showing with a Style.Setter on the Visibility property:
<ComboBox ItemsSource="{Binding ElementName=BVTWindow, Path=DataContext.AreaList}" SelectedItem="{Binding Path=Area}">
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}">
<Style.Setters>
<Setter Property="Visibility" Value="Collapsed" />
</Style.Setters>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=BVTWindow, Path=DataContext.IdentitySelection}" Value="Test Management">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
This works great. Unfortunately, applying the style setter (for some reason) removes the color theme from the ComboBox. Now it's a light grey box with white text and nearly unreadable.
I tried adding Foreground and Background attributes to the ComboBox tag, but it had no effect.
I also tried adding Setter Properties for Foreground and Background to the DataTrigger, also with no effect.
I need to either stop the theme from being removed from the box, manually set the text color, or manually set the background color.
I read one article saying that Windows 8 (which is what I'm using) interferes with ComboBox styling, but there wasn't an easily-understandable solution for it.
Any advice would help me out a lot. Thanks in advance.
This articles explains it pretty well:
http://social.technet.microsoft.com/wiki/contents/articles/24240.changing-the-background-color-of-a-combobox-in-wpf-on-windows-8.aspx
Basically, Windows 8 has a different setup for ComboBox, and it breaks some of the style attributes. You can still style it, but you've got to right click the ComboBox and pick "edit template". This will generate a massive amount of xaml in your project view (color schemes for every possible combination of states), but you probably don't need all of it. I played with commenting / uncommenting sections to figure out what each Trigger and Setter affected, then set my colors and killed the extra parts.
I have a new solution that's much cleaner, faster, easier and better-looking than my previous idea:
Just wrap the ComboBox in a WrapPanel or StackPanel and apply the Style Setter to the parent. The control will retain it's proper style and the parent panel should be transparent anyway, so styles won't matter there. Worst case, you've got a WrapPanel with only one item in it and two extra lines of code in the xaml. This will work with other controls as well, as I just had to do it with a Checkbox.
Here's an example:
<WrapPanel>
<WrapPanel.Style>
<Style TargetType="{x:Type WrapPanel}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding DataContext.TesterIdentitySelection.CanEdit, ElementName=BVTWindow}" Value="true">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</WrapPanel.Style>
<ComboBox ItemsSource="{Binding Path=DataContext.AreaList, ElementName=BVTWindow}" DisplayMemberPath="Name" SelectedItem="{Binding Path=Area, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" ToolTip="Select the testing area."/>
</WrapPanel>
The combo box is inside the wrap panel, and the wrap panel is what has the show/hide behavior applied to it. Since the wrap panel doesn't really have any style itself (just an invisible holder), everything ends up looking great.
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.