UWP style trigger missing - c#

It seems that UWP XAML doesn't support triggers in styles. What is the common workaround to accomplish triggers like the following?
<Style TargetType="Button">
<Style.Triggers>
<Trigger Property="Visibility" Value="Collapsed">
<Setter Property="Text" Value="" />
</Trigger>
</Style.Triggers>
</Style>
At the moment I see the following options to accomplish triggers in UWP:
Use Animations or VisualStateTriggers. Both seem to be wrong if I use them not to adjust the controls to the screen.
I think I found the correct way to implement Triggers in general for Controls.
See the below code as demonstration:
xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
xmlns:Core="using:Microsoft.Xaml.Interactions.Core"
<Border x:Name="BackgroundElement" Tag="Text">
<Interactivity:Interaction.Behaviors>
<Core:DataTriggerBehavior Binding="{Binding Tag, ElementName=BackgroundElement}" Value="Text">
<Core:ChangePropertyAction PropertyName="BorderBrush" Value="AliceBlue" />
</Core:DataTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</Border>
It would be awesome if there is a solution without ElementName. I would have done this in WPF with AncestorType, but that's missing in UWP too. Anyway, it seems that you can't use the Core:DataTriggerBehavior in styles.

In WinRT, RelativeSourceMode only support Self and TemplatedParent mode, FindAncestor is not available. So when you use XAML Behaviors, you need to use ElementName as a workaround. And if you are using DataContext or ViewModel in your project, you can bind to the DataContext or ViewModel to avoid using ElementName. For example:
<Page ...>
<Page.Resources>
<local:MyViewModel x:Key="ViewModel" />
</Page.Resources>
...
<Border x:Name="BackgroundElement" DataContext="{Binding Source={StaticResource ViewModel}}">
<Interactivity:Interaction.Behaviors>
<Core:DataTriggerBehavior Binding="{Binding Tag}" Value="Text">
<Core:ChangePropertyAction PropertyName="Background" Value="Red" />
</Core:DataTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</Border>
...
</Page>
And the ViewModel used above:
public class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string _tag;
public string Tag
{
get
{
return _tag;
}
set
{
_tag = value;
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs("Tag"));
}
}
}
}

Related

How to persistently change color of ListBox SelectedItem after selecting

I have a listbox that loads it's items with Foreground color set to red. What I'd like to do is: upon selecting an item with the mouse, change the foreground color of SelectedItem to black, but make the change persistent so that after deselecting the item, color remains black. Incidentally I want to implement this as a way of showing 'read items' to the user.
Essentially I want something like an implementation of the common property trigger like the code below, but not have the style revert after deselection. I've played around with event triggers as well without much luck.
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True" >
<Setter Property="Foreground" Value="Black" /> //make this persist after deselection
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
Thanks in advance!
You could animate the Foreground property:
<ListBox>
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Foreground" Value="Red" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(ListBoxItem.Foreground).(SolidColorBrush.Color)"
To="Black" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
The downside of this simple approach is that the information is not stored somewhere. This is pure visualization without any data backing. In order to persist the information, so that restarting the application shows the same previous state, you should introduce a dedicated property to your data model e.g IsMarkedAsRead.
Depending on your requirements, you can override the ListBoxItem.Template and bind ToggleButton.IsChecked to IsMarkedAsRead or use a Button which uses a ICommand to set the IsMarkedAsRead property. There are many solutions e.g. implementing an Attached Behavior.
The following examples overrides the ListBoxItem.Template to turn the ListBoxItem into a Button. Now when the item is clicked the IsMarkedAsRead property of the data model is set to true:
Data model
(See Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern for an implementation example of the RelayCommand.)
public class Notification : INotifyPropertyChanged
{
public string Text { get; set; }
public ICommand MarkAsReadCommand => new RelayCommand(() => this.IsMarkedAsRead = true);
public ICommand MarkAsUnreadCommand => new RelayCommand(() => this.IsMarkedAsRead = false);
private bool isMarkedAsRead;
public bool IsMarkedAsRead
{
get => this.isMarkedAsRead;
set
{
this.isMarkedAsRead = value;
OnPropertyChanged();
}
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
ListBox
<ListBox ItemsSource="{Binding Notifications}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Background="{TemplateBinding Background}">
<Button x:Name="ContentPresenter"
ContentTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ListBox}, Path=ItemTemplate}"
Content="{TemplateBinding Content}"
Command="{Binding MarkAsReadCommand}"
Foreground="Red">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border>
<ContentPresenter />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</Border>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding IsMarkedAsRead}" Value="True">
<Setter TargetName="ContentPresenter" Property="Foreground" Value="Green" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type Notification}">
<TextBlock Text="{Binding Text}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Thanks a lot #BionicCode for the comprehensive answer. I ended up going with another solution which may or may not be good convention; I am a hobbyist.
Firstly, I don't need databacking / persistence.
Concerning the data model solution and overriding ListBoxItem.Template, I am using a prededfined class 'SyndicationItem' as the data class (my app is Rss Reader). To implement your datamodel solution I guess I could hack an unused SyndicationItem property, or use SyndicationItem inheritance for a custom class (I'm guessing this is the most professional way?)
My complete data model is as follows:
ObservableCollection >>> CollectionViewSource >>> ListBox.
Anyway I ended up using some simple code behind which wasn't so simple at the time:
First the XAML:
<Window.Resources>
<CollectionViewSource x:Key="fooCollectionViewSource" Source="{Binding fooObservableCollection}" >
<CollectionViewSource.SortDescriptions>
<scm:SortDescription PropertyName="PublishDate" Direction="Descending" />
</CollectionViewSource.SortDescriptions>
</CollectionViewSource>
<Style x:Key="DeselectedTemplate" TargetType="{x:Type ListBoxItem}">
<Setter Property="Foreground" Value="Gray" />
</Style>
</Window.Resources>
<ListBox x:Name="LB1" ItemsSource="{Binding Source={StaticResource fooCollectionViewSource}}" HorizontalContentAlignment="Stretch" Margin="0,0,0,121" ScrollViewer.HorizontalScrollBarVisibility="Disabled" >
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="80" />
</Grid.ColumnDefinitions>
<TextBlock MouseDown="TextBlock_MouseDown" Grid.Column="0" Text="{Binding Path=Title.Text}" TextWrapping="Wrap" FontWeight="Bold" />
<TextBlock Grid.Column="1" HorizontalAlignment="Right" TextAlignment="Center" FontSize="11" FontWeight="SemiBold"
Text="{Binding Path=PublishDate.LocalDateTime, StringFormat='{}{0:d MMM, HH:mm}'}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Now the code behind:
Solution 1: this applies a new style when listboxitem is deselected. Not used anymore so the LB1_SelectionChanged event is not present in the XAML.
private void LB1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.RemovedItems.Count != 0)
{
foreach (var lbItem in e.RemovedItems)
{
//get reference to source listbox item. This was a pain.
int intDeselectedItem = LB1.Items.IndexOf(lbItem);
ListBoxItem lbi = (ListBoxItem)LB1.ItemContainerGenerator.ContainerFromIndex(intDeselectedItem);
/*apply style. Initially, instead of applying a style, I used mylistboxitem.Foreground = Brushes.Gray to set the text color.
Howver I noticed that if I scrolled the ListBox to the bottom, the text color would revert to the XAML default style in my XAML.
I assume this is because of refreshes / redraws (whichever the correct term). Applying a new style resolved.*/
Style style = this.FindResource("DeselectedTemplate") as Style;
lbi.Style = style;
}
}
}
Solution 2: The one I went with. Occurs on SelectedItem = true, same effect as your first suggestion.
private void TextBlock_MouseDown(object sender, MouseButtonEventArgs e)
{
TextBlock tb = e.Source as TextBlock;
tb.Foreground = Brushes.Gray;
}

WPF MainWindow toggle property in UserControl via ViewModel

I have a UserControl within a frame on it's parent window. In the usercontrol, I have a textbox that needs to be edited when a button on the parent window is toggled.
UserControl.xaml
<UserControl.Resources>
<ResourceDictionary>
<Style x:Key="TextBoxEdit" TargetType="TextBox">
<Setter Property="IsReadOnly" Value="True" />
<Style.Triggers>
<DataTrigger Binding="{Binding CanEdit}" Value="True">
<Setter Property="IsReadOnly" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<TextBox
x:Name="EditTextBox"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Style="{StaticResource TextBoxEdit}"
Text="Edit me" />
</Grid>
MainWindow.xaml
<Controls:MetroWindow.DataContext>
<local:ViewModel/>
</Controls:MetroWindow.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<ToggleButton x:Name="EditButton" HorizontalAlignment="Center" VerticalAlignment="Top" IsChecked="{Binding CanEdit}">Edit</ToggleButton>
<Frame Grid.Row="1" Source="Home.xaml" />
</Grid>
ViewModel
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool canEdit;
public bool CanEdit
{
get { return canEdit; }
set
{
canEdit = value;
OnPropertyChanged("CanEdit");
}
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
How do I get the data trigger to work properly? Is the best way to create another view model for the usercontrol and then communicate the values between the 2 viewmodels? If so, how would I do that?
Is the best way to create another view model for the usercontrol and then communicate the values between the 2 viewmodels?
The most common way would be for the UserControl to simply inherit the DataContext of the parent window so they both can bind to the same property.
This doesn't work out-of-the-box when you are using a Frame though.
You could either replace the Frame with a ContentControl:
<ToggleButton x:Name="EditButton" IsChecked="{Binding CanEdit}">Edit</ToggleButton>
<ContentControl>
<local:Home />
</ContentControl>
Or you could handle the DataContextChanged event for the Frame and set the DataContext of its Content explicitly as suggested by #Joe White here: page.DataContext not inherited from parent Frame?

WPF Change Window Layout Based on Combo Box Selection Using MVVM

I need to change the layout of my window based on what the user selects in a combo box. I've made a stab at what one way might be but feel like it is clunky and hacked together. Im certain their must be a cleaner MVVM solution.
My thoughts where to have multiple dock panels in my GroupBox Whose visibility is set to collapse. When the selection is made, the appropriate dockpanel will be set to visible. I attempted to find a way to do this inside the view model with no success. I also couldn't help but think my attempts are violating MVVM.
XAML
<GroupBox Header="Options">
<Grid>
<DockPanel LastChildFill="False" x:Name="syncWellHeadersDockPanel" Visibility="Collapsed">
<Button DockPanel.Dock="Right" Content="Test"></Button>
</DockPanel>
<DockPanel LastChildFill="False" x:Name="SyncDirectionalSurveyDockPanel" Visibility="Collapsed">
<Button DockPanel.Dock="Left" Content="Test02"></Button>
</DockPanel>
</Grid>
</GroupBox>
ViewModel - Property for Selected Item for ComboBox
private StoredActionsModel _selectedStoredAction = DefaultStoredAction.ToList<StoredActionsModel>()[0];
public StoredActionsModel SelectedStoredAction
{
get { return _selectedStoredAction; }
set
{
if (value != _selectedStoredAction)
{
// Unset Selected on old value, if there was one
if (_selectedStoredAction != null)
{
_selectedStoredAction.Selected = false;
}
_selectedStoredAction = value;
// Set Selected on new value, if there is one
if (_selectedStoredAction != null)
{
_selectedStoredAction.Selected = true;
}
OnPropertyChanged("SelectedStoredAction");
if (_selectedStoredAction.StoredActionID == 4)
{
//X:SyncWellHeaderDockPanel.visibility = true?????
}
}
}
}
Here's a pure-XAML way to do exactly what you're asking how to do. It's a bit verbose.
Notice that we no longer set Visibility in attributes on the DockPanels. If we still did that, the values set in the Style trigger would be overridden by the attributes. That's the way dependency properties work.
<GroupBox Header="Options">
<Grid>
<DockPanel LastChildFill="False" x:Name="syncWellHeadersDockPanel" >
<Button DockPanel.Dock="Right" Content="Test"></Button>
<DockPanel.Style>
<Style TargetType="DockPanel" >
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger
Binding="{Binding SelectedStoredAction.StoredActionID}"
Value="1"
>
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</DockPanel.Style>
</DockPanel>
<DockPanel LastChildFill="False" x:Name="SyncDirectionalSurveyDockPanel">
<Button DockPanel.Dock="Left" Content="Test02"></Button>
<DockPanel.Style>
<Style TargetType="DockPanel" >
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger
Binding="{Binding SelectedStoredAction.StoredActionID}"
Value="2"
>
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</DockPanel.Style>
</DockPanel>
</Grid>
</GroupBox>
Another way to do this would be to pass SelectedStoredAction.StoredActionID to a DataTemplateSelector, but that involves writing C# code that knows what your XAML resource keys are, and I'm not a fan.

What is the standard way to configure an ItemTemplate without violating the Item separation?

In using WPF for a while, this is actually the first time that I've come across a situation where I have an ItemTemplate for a ListBox that I want to be configurable based on properties outside the item itself. The problem came up when I wanted to have a Font selector dialog where the user could click a chechbox to enable the font previews (I actually changed my mind about this implementation, but I'd still like to know the answer).
It seems, because the DataTemplate may be used in any situation where that type is provided, it's considered good practice, not to bind to any parameters outside the item's configuration (seems like the code to bind to properties of a containing DataTemplate is particularly obtuse.)
I was wondering how I was supposed to implement this kind of situation. The code below works, but the binding is to a visual element, whereas I would rather bind to the property in the ViewModel.
<Window x:Class="ScreenWriter.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525" DataContext="{Binding Mode=OneWay, RelativeSource={RelativeSource Self}}" >
<Grid>
<StackPanel>
<CheckBox Name="ShowPreview" IsChecked="{Binding IsShowPreviewChecked}">
Show Preview
</CheckBox>
<ListBox ItemsSource="{Binding Source={x:Static Fonts.SystemFontFamilies}}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}">
<TextBlock.Style>
<Style>
<Setter Property="TextBlock.FontFamily" Value="Arial" />
<Style.Triggers>
<DataTrigger Value="True" Binding="{Binding IsChecked, ElementName=ShowPreview}">
<Setter Property="TextBlock.FontFamily" Value="{Binding}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</Grid>
</Window>
This doesn't seem like an unusual situation, but I can't find any solution that isn't prefixed with "this is a clever way to get around what you're not supposed to do".
Thanks for any help...
There are no 'best practice' solution for this problem.
As you wrote, "the DataTemplate may be used in any situation where that type is provided ... not to bind to any parameters outside the item's configuration".
Binding/DataContext are based on LogicalTree. So your Trigger binding trys to find the closest datacontext on the tree (in this case your ListItem DataContext = FontFamily )
You have to make same 'jump' from this 'LogicalTree Island' to the main Tree - like your control/element did a jump.
So the real question is, how could you change the binding source smoothly.
You could bind Target element DataContext directly (CheckBox has no DataContext, but its grandparent - Windows has):
<DataTrigger Binding="{Binding DataContext.IsShowPreviewChecked, ElementName=ShowPreview}" Value="True" >
or just find ListItem ancestor directly:
<DataTrigger Binding="{Binding DataContext.IsShowPreviewChecked, RelativeSource={RelativeSource AncestorType=ListBox, Mode=FindAncestor}}" Value="True" >
If you will use this jump many times, you will try to use BindingProxy
which will provide a shortcut for your bindings:
public class BindingProxy : Freezable
{
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
public object Context
{
get { return (object)GetValue(ContextProperty); }
set { SetValue(ContextProperty, value); }
}
public static readonly DependencyProperty ContextProperty =
DependencyProperty.Register("Context", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
Add this proxy as resouce:
<Window.Resources>
<this:BindingProxy x:Key="Proxy" Context="{Binding}" />
</Window.Resources>
and your binding is:
<DataTrigger Binding="{Binding Context.IsShowPreviewChecked, Source={StaticResource Proxy}}" Value="True" >
Do not forget add INotifyPropertyChanged Interface to your ViewModel

How to change the style of a button based on if else using DataTriggers in wpf mvvm

I want to change the style of the button on the basis of if else condition when first the wpf application is getting loaded. On application loaded using if, there will be one style of button and in else part, there will be another. How to achieve this using Datatriggers or else using MVVM pattern.
Kindly Suggest?
Thanks
You can use Style.Setters to set default value. For other determined conditions use Style.Triggers. This works like if else.
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=EditorWindow, Path=Category}" Value="R">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
<Style.Setters>
<Setter Property="Visibility" Value="Collapsed"/>
</Style.Setters>
</Style>
</TextBlock.Style>
Alternatively, if you want to use DataTriggers, you can use the following:
<Button Command="{Binding SomeButtonCommand}" Content="Click Me!">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=NormalButtonMode, Mode=OneWay}" Value="True">
<Setter Property="Content" Value="This Button is in Normal Mode" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=NormalButtonMode, Mode=OneWay}" Value="False">
<Setter Property="Content" Value="This Button is in the Other Mode" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
In this case the ViewModel must expose the boolean property NormalButtonMode. In this example I only set the Content property of the button, but you can list any number of Setters inside the DataTrigger.
You can also put this Style in a resource dictionary and just link it for each button using StaticResource. Just make sure you expose the NormalButtonMode (or whatever) property on each and every ViewModel - maybe put it in a base class.
You should look into Data Templates and a Template Selector. Here is a hastily copy pasted example from my own code, it's not immediately applicable to buttons but I think it should help you along your way.
The following is from the application resources xaml file. I use it to decide which view to use for the ProjectViewModel based on a variable in the ViewModel:
<DataTemplate DataType="{x:Type viewmod:ProjectViewModel}">
<DataTemplate.Resources>
<DataTemplate x:Key="ProjectEditViewTemplate">
<view:ProjectEditView/>
</DataTemplate>
<DataTemplate x:Key="ServiceSelectionViewTemplate">
<view:ServiceSelectionView/>
</DataTemplate>
</DataTemplate.Resources>
<ContentControl Content="{Binding}" ContentTemplateSelector="{StaticResource ProjectViewModelTemplateSelector}" />
</DataTemplate>
The ProjectViewModelTemplateSelector is defined as follows:
public class ProjectViewModelTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
FrameworkElement element = container as FrameworkElement;
if (element != null && item != null && item is ViewModel.ProjectViewModel)
{
if ((item as ViewModel.ProjectViewModel).EditMode)
{
return element.FindResource("ProjectEditViewTemplate") as DataTemplate;
}
else
{
return element.FindResource("ServiceSelectionViewTemplate") as DataTemplate;
}
}
else
return base.SelectTemplate(item, container);
}
}
}

Categories