DataGrid change RowDetailsTemplate dynamically - wpf

Situation:
I have a DataGrid where the items can belong to different categories, say A, B and Custom. Each Category has its own RowDetails template. The category of an item can be changed, and when this happens I want to change the template as well, if necessary. The ViewModel behind is the same, I just change the interface elements (for example, in template A I have a TextBlock, while in template B I have a TextBox, both with a Binding to the same property in the VM).
What I have done so far:
<DataGrid.RowDetailsTemplate>
<DataTemplate>
<Control x:Name="RowDetails" Focusable="False" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding TemplateID, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" Value="0">
<Setter TargetName="RowDetails" Property="Template" Value="{StaticResource TemplateA}" />
</DataTrigger>
<DataTrigger Binding="{Binding TemplateID, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" Value="1">
<Setter TargetName="RowDetails" Property="Template" Value="{StaticResource TemplateB}" />
</DataTrigger>
<DataTrigger Binding="{Binding TemplateID, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}" Value="2">
<Setter TargetName="RowDetails" Property="Template" Value="{StaticResource TemplateCustom}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</DataGrid.RowDetailsTemplate>
The problem:
When I change the category of an item, I can see that the template changes, but the entire row gets the ValidationErrorTemplate (the red border, with exclamation mark). This happens even if the templates are empty! Seems to me that just changing the template makes the whole thing to explode.
What am I doing wrong? Thanks!

After a lot of try-and-retry, I found the problem to be on a ComboBox inside my templates: as stated here, order in the properties of a ComboBox matters. Putting SelectedValue before ItemsSource did the trick for me, and the approach using the Triggers works like charm.
Nevertheless, I'm still shocked by such solution. I'll mark this as the answer, but I'm still open to suggesions.

Related

wpf visibility based on a condition

I want to show a StackPanel based on a particular condition. In this example I've used the BorderThickness property:
<ContentControl x:Name="gridDati" VirtualizingPanel.VirtualizationMode="Recycling" VirtualizingPanel.ScrollUnit="Item" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Items}" Value="{x:Null}">
<Setter Property="BorderThickness" Value="0" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Items.Count}" Value="0">
<Setter Property="BorderThickness" Value="12" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" x:Name="pnlLoading" Visibility="Visible">
<Label Content="">
<Label.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=BorderThickness, ElementName=gridDati, UpdateSourceTrigger=PropertyChanged}" Value="0">
<Setter Property="TextBlock.Text" Value="" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=BorderThickness, ElementName=gridDati, UpdateSourceTrigger=PropertyChanged}" Value="12">
<Setter Property="TextBlock.Text" Value="STAND BY" />
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
</StackPanel>
Basically when in the code behind I apply a template on gridDati, while the item counter is still zero, the border is set correctly to 12. After that it turns to zero (item binded) and this behevior is what I want.
So, I also would like to show a StackPanel at the same condition, so I used a DataTrigger but seems that is not fired at all. How can I "link" these two condition? so show a stackpanel when I have items in the datagrid?
This is the proper way to declare the Label so you get the desired result.
<Label>
<Label.Style>
<Style TargetType="Label">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Items.Count}" Value="0">
<Setter Property="Content" Value="STAND BY"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
But there are a few points I need to explain to make sure you understand what I changed and why. I'm going to go through the XAML from the inside out.
First, I changed the Setter to use the right property name. You're using a Label, and your old Setter had Property="TextBlock.Text". TextBlock.Text is not a valid property name for a Label (no such property exists), so that wasn't going to work. The property you want is called Content.
Moving up one level to the DataTrigger. Instead of binding to gridDati that is binding to Items, I just bound directly to Items. You could do it the other way, but in my opinion it would be unusual and it might cause unforeseen bugs.
Next, you'll notice I removed the first DataTrigger. WPF dependency properties can be set in a number of different ways, and there is an order of precedence for which value will be taken over others. The default value (lowest precedence) for a Label's content is for it to be empty. When the DataTrigger applies the Setter, it overrides that value (it has higher precedence). When the DataTrigger condition is no longer fulfilled (Items.Count != 0), WPF stops applying the Setter and the value reverts back to the default, because there is no longer any value of higher precedence overriding it. So you don't need to add a second DataTrigger resetting to default, wit ill do that automatically.
Moving up further you'll see I changed the opening Style tag to <Style TargetType="Label">. It's common practice to set the TargetType of a Style. Doing this also gives you IntelliSense options for Setters in that Style, which might have helpped you catch the mistake you made by trying to use TextBlock.Text as a property name.
Finally, I removed Content="" from the opening Label tag. Setting the value of a property directly on an element in XAML has a very high precedence, which overrides all Styles and DataTriggers. As long as this was there, nothing you did in any Style would change anything for the Label's Content.

Binding errors on ContentControl after changing Content before ContentTemplate is applied

I'm having an annoying issue that is not exactly causing problems, but it is generating a ton of binding errors unnecessarily.
I've basically tracked the problem down to the fact that setting the Content on a ContentControl changes the DataContext of its content before it applies the new ContentTemplate. Since the new Content is not of the same type as the old ContentTemplate expects, it generates binding errors from the old ContentTemplate.
Here's how I have the ContentControl set up. The Content is bound to the ViewModel for the selected tab, and the ContentTemplate is bound to the DataTemplate with the View for that tab. I used to have it using a ContentTemplateSelector instead of a converter with ContentTemplate, but that had the same issues so I tried this instead.
<ContentControl Content="{Binding SelectedTab, Converter={StaticResource ConfigurationViewModelConverter}}" ContentTemplate="{Binding SelectedTab, Converter={StaticResource ConfigurationTemplateConverter}}"/>
Perhaps I've got this wired up wrong somehow but everything is working perfectly with the exception of the binding errors I get when switching tabs, seemingly due to the Content and ContentTemplate getting briefly out of sync. Thanks in advance for any assistance.
So, I'm almost 2 years late on this, but I had been stuck on this exact same issue for about a day so I figured I'd share. I had 3 types of view models corresponding to 3 different UserControls/DataTemplates. I used a style to fix this
<ContentControl>
<ContentControl.Resources>
<DataTemplate x:Key="fooUc">
<local:UC1 />
</DataTemplate>
<DataTemplate x:Key="barUc">
<local:UC2 />
</DataTemplate>
<DataTemplate x:Key="bazUc">
<local:UC3 />
</DataTemplate>
</ContentControl.Resources>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<!-- I'm assuming that SelectedTab is an int -->
<DataTrigger Binding="{Binding SelectedTab}" Value="0">
<Setter Property="Content" Value="{Binding FooVm}" />
<Setter Property="ContentTemplate" Value="{DynamicResource fooUc}" />
</DataTrigger>
<DataTrigger Binding="{Binding SelectedTab}" Value="1">
<Setter Property="ContentTemplate" Value="{DynamicResource barUc}" />
<Setter Property="Content" Value="{Binding BarVm}" />
</DataTrigger>
<DataTrigger Binding="{Binding SelectedTab}" Value="2">
<Setter Property="ContentTemplate" Value="{DynamicResource bazUc}" />
<Setter Property="Content" Value="{Binding BazVm}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
The crucial thing to notice above is the ordering of my setters. If your view object must be changed before the view model object, then put it before, otherwise flip the ordering. The problem is that one has to be changed before the other, they can't change at the exact same time. If your controls are completely different from one another, then this will still produce errors (different ones though!). This worked for me because BarVm and BazVm have a subset of properties as FooVm.
I had the same problem and I ended up detaching all bindings from the old control before doing the view/viewmodel switch.
Hope this helps someone else with same problem.
public static void ClearAllBindings(DependencyObject obj)
{
if (obj == null)
return;
foreach (var child in LogicalTreeHelper.GetChildren(obj))
{
if (!(child is DependencyObject dp))
continue;
BindingOperations.ClearAllBindings(dp);
ClearAllBindings(dp);
}
}

Binding to the Items properties of an ItemsControl

I'm looking to a way to bind a Button Text or IsEnabled property for example to the IsChecked property in an ItemsControl
Here is my simplified source code :
<StackPanel>
<ItemsControl ItemsSource="{Binding Tasks}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="10,0,10,0" Text="{Binding Name}"/>
<CheckBox IsChecked="{Binding InProgress}"/>
<CheckBox IsChecked="{Binding Done}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button/>
</StackPanel>
I would like (for example) if all the "Done" CheckBoxes are Checked, to set Button Text to some value or to enable it. I thought doing this with Data Binding in Xaml using DataTriggers but I don't know how to do it.
Ca anyone give me a full xaml solution ?
New Answer
Sorry, I misunderstood the question. I would expose another property from your DataContext that simply returns true/false if all the items in the collection are checked or not, and base your Button's Text/IsEnabled off that property using a DataTrigger
<Style TargetType="{x:Type Button}">
<Setter Property="IsEnabled" Value="False" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsAllChecked}" Value="True">
<Setter Property="IsEnabled" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
Old Answer
DataTriggers simply take a binding, and check if the result is equal to some value.
<Style TargetType="{x:Type TextBox}">
<Setter Property="IsEnabled" Value="False" />
<Style.Triggers>
<DataTrigger Binding="{Binding Done}" Value="True">
<Setter Property="IsEnabled" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
If you need to test more than one condition, you need a MultiDataTrigger
<Style TargetType="{x:Type TextBox}">
<Setter Property="IsEnabled" Value="False" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Path=InProgress}" Value="True" />
<Condition Binding="{Binding Path=Done}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
Note that the default value of setter is part of the style. This is important because if you set the default value on the <TextBox> then it has a higher priority than triggered values, so triggered values are unable to change the current value.
You can change this line...
<Button/>
To this
<Button Content={Binding ButtonContent}/>
And set the 'ButtonContent' property in your ViewModel whenever your 'Done' check box gets checked.
Usually, the best way to do that is to synthesize the state you want to apply to your button into a property of your ViewModel.
The WPFy way in that case is for your ViewModel to expose a command that implements the action of your button.
The command in turns can react to changes in your data to signal if it is available or not, and thus will automaticaally disable/enable your button. An added bonus is that you can easily implement a menu for example, that will be properly enabled/disabled at no cost once you have your command.
It's been a long time since I had any chance to touch WPF, so I do not have any really useful links at hand, but if I were you I would learn about commands in WPF, they are a very important part of the MVVM architecture.

DataTrigger in CellTemplate binding to HeaderTemplate; can it work?

The goal here would be to check all grid checkboxes if the header checkbox changes:
<Window.Resources>
<Style TargetType="CheckBox" x:Key="InnerBox">
<Setter Property="HorizontalAlignment" Value="Center" />
<Style.Triggers>
<DataTrigger Value="True"
Binding="{Binding IsChecked,
ElementName=HeaderCheckbox}">
<Setter Property="IsChecked" Value="True" />
</DataTrigger>
<DataTrigger Value="False"
Binding="{Binding IsChecked,
ElementName=HeaderCheckbox}">
<Setter Property="IsChecked" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<DataGrid>
<DataGrid.Columns>
<!-- col1 -->
<DataGridTemplateColumn>
<DataGridTemplateColumn.HeaderTemplate>
<DataTemplate>
<!-- header check -->
<CheckBox Name="HeaderCheckbox" />
</DataTemplate>
</DataGridTemplateColumn.HeaderTemplate>
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<!-- body check -->
<CheckBox Style="{StaticResource InnerBox}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<!-- col2 -->
<DataGridTextColumn Binding="{Binding}" Header="Text" />
</DataGrid.Columns>
<!-- sample data -->
<sys:String>1</sys:String>
<sys:String>2</sys:String>
<sys:String>3</sys:String>
</DataGrid>
Looks like:
For some reason, the trigger does not fire.
Any ideas?
ElementName binding inside a DataTemplate can't reach an element outside of the template as you noticed. This is because it can be instantiated many times and has its own namescope so any ElementName binding you create inside a DataTemplate will look inside the template for another element with that name.
Looking at it with Snoop we can also see that a RelativeSource binding can't be used directly since they are in different parts of the Visual Tree
The only thing that I can think of to get around this is to bind both of the CheckBoxes to a common ancestor, e.g. the parent DataGrid and use an attached property or the Tag property. Example
<Style TargetType="CheckBox" x:Key="InnerBox">
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="IsChecked" Value="False" />
<Style.Triggers>
<DataTrigger Value="True"
Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},
Path=Tag}">
<Setter Property="IsChecked" Value="True" />
</DataTrigger>
</Style.Triggers>
</Style>
and
<DataTemplate>
<!-- header check -->
<CheckBox Name="HeaderCheckbox"
IsChecked="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},
Path=Tag,
Mode=OneWayToSource}"/>
</DataTemplate>
I don't think regular DataBinding to the HeaderCheckBox is possible because the CheckBox exists as part of a Template, and it is in a different branch of the VisualTree than the DataGridItems
Usually I make it the reverse: When the header CheckBox gets checked, check all the row CheckBoxes. My main reason for this is because the CheckBoxes are usually there so users can check/uncheck them, and if they're bound to the header CheckBox checked state, then the user can't alter them.
For implementing that, I usually hook into the Click or Checked event of the Header CheckBox.
If the row CheckBox.IsChecked state is bound to something in a ViewModel, I'll hook the event to a Command in my ViewModel, and set the data item that the CheckBox.IsChecked is bound to to true/false depending on the header CheckBox state (usually passed in as a CommandParameter)
If the CheckBox.IsChecked state is not bound to anything, you can use regular code-behind to loop through your DataGrid.Items, use the ItemContainerGenerator to get the ItemContainer for each item, find the CheckBox, and then set it's check state.

Databinding to XML in a DataTrigger in WPF

In a WPF application, I have correctly bound a DataTemplate to an XML node that looks like:
<answer answer="Tree", correct="false" score="10" />
In my application, I have a TextBlock with the answer in it. At first, I want it invisible, but when the correct attribute in the XML file changes to "true", it must become visible.
My DataTemplate is hooked up correctly, because everything else works. For example, if I change the answer attribute in the XML file (just for testing), it changes in my WPF view. But I'm having troubles with the visibility. This is my XAML:
<TextBlock Text="{Binding XPath=#answer}" Visibility="Hidden">
<TextBlock.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding XPath=#correct}" Value="true">
<Setter Property="TextBlock.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
I'm guessing the Databinding in the DataTrigger isn't working correctly. Anyone have a clue?
I have run into the same problem with databound ToggleButtons. Try removing the Visibility="False" and replacing it with another DataTrigger that handles the incorrect case.
I think the issue is that the Visibility property is hard-coded. Try setting the Visibility in the style:
<TextBlock Text="{Binding XPath=#answer}">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility" Value="Hidden"/>
<Style.Triggers>
<DataTrigger Binding="{Binding XPath=#correct}" Value="true">
<Setter Property="TextBlock.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
Sure, it works if you give a specific else case instead of just false. As in my case, it was {x:Null} and value. So when its value to bind is present, it will be true and TextBlock.Visibilty will be set using setters value and when binding path does not have any value inside it, i.e. null in my case, its simply {x:Null} :)

Resources