I'm trying to implement a hamburger button by myself in a Windows 10 app. I'm running into a little trouble with my ResourceDictionary when trying to set the Command property of a Button (via a style). Here is my code:
Hamburger.xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Octopie.Styles.Hamburger"
xmlns:local="using:Octopie.Styles">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Square.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style x:Key="HamburgerStyle" TargetType="Button" BasedOn="{StaticResource SquareStyle}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Command" Value="{Binding OnClicked}"/> <!--This is the part that's having issues-->
<Setter Property="Content" Value=""/>
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
</Style>
</ResourceDictionary>
Hamburger.xaml.cs
namespace Octopie.Styles
{
public sealed partial class Hamburger : ResourceDictionary
{
public Hamburger()
{
this.InitializeComponent();
}
public ICommand OnClicked => new ClickedCommand();
private class ClickedCommand : ICommand
{
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) =>
parameter is Button;
public void Execute(object parameter)
{
var button = (Button)parameter;
// Walk up the tree until we reach a SplitView
FrameworkElement parent = button;
do
parent = parent.Parent as FrameworkElement;
while (!(parent is SplitView));
var splitView = (SplitView)parent;
splitView.IsPaneOpen = !splitView.IsPaneOpen;
}
}
}
}
For some reason the binding for the Command property doesn't seem to be working; when I set a breakpoint inside the Execute method and click the button, the breakpoint is never hit. I tried adding a DataContext="{Binding RelativeSource={RelativeSource Self}}" to the top of the XAML file, but for some reason ResourceDictionary doesn't seem to support DataContext.
tl;dr: What can I do to make the Button.Command property bind correctly to OnClicked within the setter?
Like Mike said, usually we won't set Button.Command in ResourceDictionary. A hamburger button may not only be in SplitView but can be in another place and then you may need bind another command. So you can refer to Mike's suggestion.
But if you do want to set it in ResourceDictionary, you can try like following:
Firstly, in your case, your command is fixed, you can declare your ClickedCommand as a public class, then in the Style,set the Command like:
<Setter Property="Command">
<Setter.Value>
<local:ClickedCommand />
</Setter.Value>
</Setter>
After this, you can use your command, but this won't fix your problem as in ClickedCommand, you use parameter to retrieve the Button, but the parameter is not the "sender" of the Command, but the object passed with CommandParameter property. So we need set this in the Style.
However, Bindings in Style Setters are not supported in UWP Apps. See Remarks in Setter class:
The Windows Runtime doesn't support a Binding usage for Setter.Value (the Binding won't evaluate and the Setter has no effect, you won't get errors, but you won't get the desired result either).
A workaround for this is using attached property to set up the binding in code behind for you. For example:
public class BindingHelper
{
public static readonly DependencyProperty CommandParameterBindingProperty =
DependencyProperty.RegisterAttached(
"CommandParameterBinding", typeof(bool), typeof(BindingHelper),
new PropertyMetadata(null, CommandParameterBindingPropertyChanged));
public static bool GetCommandParameterBinding(DependencyObject obj)
{
return (bool)obj.GetValue(CommandParameterBindingProperty);
}
public static void SetCommandParameterBinding(DependencyObject obj, bool value)
{
obj.SetValue(CommandParameterBindingProperty, value);
}
private static void CommandParameterBindingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
BindingOperations.SetBinding(d, Button.CommandParameterProperty, new Binding { RelativeSource = new RelativeSource() { Mode = RelativeSourceMode.Self } });
}
}
}
Then in Style, using
<Setter Property="local:BindingHelper.CommandParameterBinding" Value="True" />
will set the Button as CommandParameter. Your Hamburger.xaml may like:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Octopie.Styles">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Square.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style x:Key="HamburgerStyle" TargetType="Button" BasedOn="{StaticResource SquareStyle}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Command">
<Setter.Value>
<local:ClickedCommand />
</Setter.Value>
</Setter>
<Setter Property="local:BindingHelper.CommandParameterBinding" Value="True" />
<Setter Property="Content" Value="" />
<Setter Property="FontFamily" Value="Segoe MDL2 Assets" />
</Style>
</ResourceDictionary>
I delete x:Class="Octopie.Styles.Hamburger" and Hamburger.xaml.cs as there is no need to use code-behind for your ResourceDictionary.
Now we can use this ResourceDictionary in our page like:
<Page.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Hamburger.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<SplitView DisplayMode="CompactOverlay" IsPaneOpen="True">
<SplitView.Pane>
<StackPanel>
<Button Style="{StaticResource HamburgerStyle}" />
</StackPanel>
</SplitView.Pane>
</SplitView>
</Grid>
But there is another problem in Execute method of ClickedCommand. In this method, you've used FrameworkElement.Parent to retrieve the SplitView. But
Parent can be null if an object was instantiated, but is not
attached to an object that eventually connects to a page object root.
Most of the time, Parent is the same value as returned by
VisualTreeHelper APIs. However, there may be cases where Parent
reports a different parent than VisualTreeHelper does.
And in your case, you need use VisualTreeHelper.GetParent to get the SplitView. We can use a helper method to do this:
public static T FindParent<T>(DependencyObject child) where T : DependencyObject
{
//get parent item
DependencyObject parentObject = VisualTreeHelper.GetParent(child);
//we've reached the end of the tree
if (parentObject == null) return null;
//check if the parent matches the type we're looking for
T parent = parentObject as T;
if (parent != null)
return parent;
else
return FindParent<T>(parentObject);
}
Then in Execute method using:
public void Execute(object parameter)
{
var button = (Button)parameter;
var splitView = FindParent<SplitView>(button);
splitView.IsPaneOpen = !splitView.IsPaneOpen;
}
Now the HamburgerStyle will work as you want.
What the hell?
You're going about this all wrong. You don't need to declare a new ICommand in a ResourceDictionary, it simply doesn't belong there. It belongs in your View Model, or whatever the Button.DataContext is set to.
The purpose of a Style is to control the look and feel of your controls, they should not explicitly set their own behaviours (commands).
Let me show you an example. You should declare your button like this:
<Button Style="{StaticResource HamburgerStyle}" Command="{Binding ClickedCommand}"/>
Where ClickedCommand is an object in your View Model.
Your HamburgerStyle should not set it's own Command property, otherwise you are limiting your Button to one single implementation of ICommand, this is unwise.
Related
I have a user control for which I have to change color, based on mouse hover, click or none. Following MVVM. This is the code I have:
User control in XAML
<userControls:NC DataContext="{Binding NCVM}" >
</userControls:NC>
User Control View Model
public class NCVM : ObservableObject
{
public NCVM()
{
}
private NCState _currentState = NCState.InActive;
public NCState CurrentState
{
get => _currentState;
set
{
_currentState = value;
switch (_currentState)
{
case NCState.InActive:
ForegroundColor = System.Windows.Media.Brushes.LightGray;
IsActive = false;
break;
case NCState.Active:
ForegroundColor = System.Windows.Media.Brushes.White;
IsActive = true;
break;
case NCState.Hovered:
ForegroundColor = System.Windows.Media.Brushes.White;
IsActive = false;
break;
default:
ForegroundColor = System.Windows.Media.Brushes.LightGray;
IsActive = false;
break;
}
}
}
public bool _isActive;
public bool IsActive
{
get => _isActive;
set => SetProperty(ref _isActive, value);
}
private System.Windows.Media.Brush _foregroundColor = System.Windows.Media.Brushes.LightGray;
public System.Windows.Media.Brush ForegroundColor
{
get => _foregroundColor;
set => SetProperty(ref _foregroundColor, value);
}
}
Main Window View Model
public class MWVM : BVM
{
#region Private Variables
private NCVM _NCVM = new();
#endregion
public MWVM()
{
NCVM.CurrentState = NCState.Active;
}
#region Public Properties
public NCVM NCVM
{
get => _NCVM;
set => SetProperty(ref _NCVM, value);
}
#endregion
}
Right now, it's getting preset as active for checking. Now, I have to make it manual so it changes on hover, but not getting how to do with binding.
The MVVM pattern is about separating the user interface (view) from the data and application logic itself. Your example violates MVVM in that it stores the brushes and the visual states in a view model. The view model should only expose data and commands to be bound, but not user interface elements and it must not contain logic to that relates to the user interface just like managing visual states or appearance. It is too often misunderstood as creating a view model and just putting everything there.
In your case, I think that you can solve your issue by moving everything into a style. The following XAML should show your userControls:NC. There are triggers for different states like Disabled, Hover / Mouse Over. Please note that you need to set a Background, otherwise the control does not participate in hit testing and e.g. the IsMouseOver property will not be True even if you hover over it. For no background use Transparent (which is not equal to not setting a value).
<UserControl ...>
<UserControl.Style>
<Style TargetType="{x:Type userControls:NC}">
<!-- Background must be set at least to "Transparent" -->
<Setter Property="Background" Value="Black"/>
<!-- Default -->
<Setter Property="Foreground" Value="LightGray"/>
<Style.Triggers>
<!-- Hovered -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="White"/>
</Trigger>
<!-- Disabled -->
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="LightGray"/>
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Style>
<!-- Dummy element for demonstration purposes of foreground -->
<TextBlock Text="This text shows the foreground"/>
</UserControl>
You may take a look at EventTrigger, or Triggers in general to style your control.
*Edit:
A little example, MVVM not considered, just for you to get a glimpse at triggers.
UserControl:
<UserControl x:Class="WpfApp1.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type={x:Type local:UserControl1}}"
Height="200" Width="400">
<UserControl.Style>
<Style TargetType="UserControl">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsMyPropSet}" Value="True">
<Setter Property="Background" Value="Turquoise"/>
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Style>
<GroupBox Header="I am your usercontrol">
<Button Width="100" Height="35" Content="Toggle Property" Click="Button_Click"/>
</GroupBox>
</UserControl>
and code-behind:
public partial class UserControl1 : UserControl, INotifyPropertyChanged
{
public UserControl1()
{
InitializeComponent();
DataContext = this;
}
public event PropertyChangedEventHandler PropertyChanged;
public bool IsMyPropSet { get; set; }
private void Button_Click(object sender, RoutedEventArgs e)
{
IsMyPropSet = !IsMyPropSet;
RaisePropertyChanged(nameof(IsMyPropSet));
}
protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
I have a Button hosted within a UserControl which I would like to change the Background and Foreground properties of whenever the numeric value of a property held within the view model of the UserControl changes.
My view model appears as follows (properties and methods removed for brevity). I am using Prism's DelegateCommand<T> for my commands.
public class PianoKeyViewModel : BindingTarget, IPianoKey
{
private byte afterTouch;
private DelegateCommand<AfterTouchEventArgs> afterTouchChangedCommand;
public byte AfterTouch
{
get
{
return afterTouch;
}
set
{
//Calls a method on BindingTarget to assign to afterTouch and call RaisePropertyChanged.
SetProperty(ref afterTouch, value);
}
}
public DelegateCommand<AfterTouchEventArgs> AfterTouchChangedCommand
{
get
{
return afterTouchChangedCommand;
}
set
{
//Calls a method on BindingTarget to assign to afterTouchChangedCommand and call RaisePropertyChanged.
SetProperty(ref afterTouchChangedCommand, value);
}
}
protected override void RaisePropertyChanged([CallerMemberName]string propertyName = null)
{
base.RaisePropertyChanged(propertyName);
switch (propertyName)
{
case nameof(AfterTouch):
AfterTouchEventArgs afterTouchEventArgs = new AfterTouchEventArgs(AfterTouch);
if (AfterTouchChangedCommand != null && AfterTouchChangedCommand.CanExecute(afterTouchEventArgs))
//Executes the AfterTouchChangedCommand since the AfterTouchProperty has just changed and the command exists and can execute.
AfterTouchChangedCommand.Execute(afterTouchEventArgs);
break;
}
}
}
A vastly cut-down version of my XAML for the UserControl is as follows. Note the line below my TODO comment - AfterTouchChanged does not exist and needs replacing with a binding to AfterTouchChangedCommand which fires the trigger when Execute is called on it. Also, the DataContext of this UserControl will be the View Model shown above, this is achieved with a Module utilizing Prism.
<UserControl x:Class="MyNamespace.PianoKey.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:converters="clr-namespace:MyNamespace.PianoKey.View.Converters"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<UserControl.Resources>
<converters:AfterTouchToBackgroundColorConverter x:Key="afterTouchToBackgroundColorConverter"/>
<converters:AfterTouchToForegroundColorConverter x:Key="afterTouchToForegroundColorConverter"/>
</UserControl.Resources>
<UserControl.Template>
<Button>
<Button.Triggers>
<!--TODO: Find out what trigger is needed here to respond to AfterTouchChangedCommand.Execute.-->
<EventTrigger RoutedEvent="AfterTouchChanged">
<Setter Property="Background" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=AfterTouch, Converter={StaticResource afterTouchToBackgroundColorConverter}}"/>
<Setter Property="Foreground" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=AfterTouch, Converter={StaticResource afterTouchToForegroundColorConverter}}"/>
</EventTrigger>
</Button.Triggers>
</Button>
</UserControl.Template>
</UserControl>
What do I need to replace the EventTrigger with to get this to update the Background and Foreground colors of the Button? DataTrigger looked promising but as far as I can tell this only works to respond to a single value on a property on the bound source.
I am learning WPF and MVVM at the moment (or at least I am trying to...).
I created a little sample-app, that shows a Window with 2 buttons, each of it should show a new View on Click. So I created 3 UserControls (DecisonMaker with the 2 Buttons, and one Usercontrol for each "clicktarget").
So I bound the CotentControl of the MainWindow to a property called "CurrentView" in my MainWindowViewModel
Code of MainWindow.xaml:
<Window x:Class="WpfTestApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfTestApplication"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<ContentControl Content="{Binding CurrentView, Mode=OneWay}" />
</Grid>
</Window>
Code of MainWindowViewModel:
class MainWindowViewModel
{
private UserControl _currentView = new DecisionMaker();
public UserControl CurrentView
{
get { return _currentView; }
set { _currentView = value; }
}
public ICommand MausCommand
{
get { return new RelayCommand(LoadMouseView); }
}
public ICommand TouchCommand
{
get { return new RelayCommand(LoadTouchView); }
}
private void LoadMouseView()
{
CurrentView = new UserControlMouse();
}
private void LoadTouchView()
{
CurrentView = new UserControlTouch();
}
}
The initial UserControl (DecisionMaker) shows up as supposed. Also the method LoadMouseView is called. But the View doesn't change. What am I missing?
UPDATE: Thanks so much! I missed the INotifyPropertyChanged-interface. All of your answers were just great and very accurate and helpful! I don't know which one to accept - I think it's the most fair way to accept the "first" answer?
I accepted blindmeis answer, as it solved the problem and helped me understand MVVM better. But every answer was really great thanks to all of you!
if you wanna do mvvm - then you should have no references to your view/usercontrols in your viewmodel. you have to implement INotifyPropertyChanged! ps: if you need System.Windows namespace in your Viewmodel - then something is wrong.
in your case what you need:
1 mainviewmodel
1 viewmodel for UserControlMouse
1 viewmodel for UserControlTouch
1 view/usercontrol for UserControlMouse
1 view/usercontrol for UserControlTouch
your mainviewmodel should have at least 2commands to switch your view and 1 property for CurrentView. in your command you simply set your CurrentView to the right viewmodel instance. at least you need two datatemplates for each viewmodel which define the right view.
public object CurrentView
{
get { return _currentView; }
set {
_currentView = value; this.RaiseNotifyPropertyChanged("CurrentView");}
}
xaml
<Window x:Class="WpfTestApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfTestApplication"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate DataType="{x:Type local:MyMouseViewModel}">
<local:MyMouseUserControlView/>
</DataTemplate>
<DataTemplate DataType="{x:Type local:MyTouchViewModel}">
<local:MyTouchUserControlView/>
</DataTemplate>
</Window.Resources>
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<!-- here your buttons with command binding, i'm too lazy to write this. -->
<!-- you content control -->
<ContentControl Content="{Binding CurrentView, Mode=OneWay}" />
</Grid>
</Window>
I would do something like this to select the input style that you want, to MainWindow I've added a property that lets me select the mode of input.
public enum UserInterfaceModes
{
Mouse,
Touch,
}
public UserInterfaceModes UserInterfaceMode
{
get { return (UserInterfaceModes)GetValue(UserInterfaceModeProperty); }
set { SetValue(UserInterfaceModeProperty, value); }
}
public static readonly DependencyProperty UserInterfaceModeProperty = DependencyProperty.Register("UserInterfaceMode", typeof(UserInterfaceModes), typeof(MainWindow), new UIPropertyMetadata(UserInterfaceModes.Mouse));
then for the xaml view part you can select the correct template with a trigger.
<Style TargetType="{x:Type local:MainWindow}">
<Style.Triggers>
<DataTrigger Binding="{Binding UserInterfaceMode}" Value="Mouse">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MainWindow}">
<Grid Background="Red"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding UserInterfaceMode}" Value="Touch">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MainWindow}">
<Grid Background="Blue"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
The viewmodel need to implement INotifyPropertyChanged. Otherwise the view won't be notified when a property changes in the viewmodel.
class MainWindowViewModel : INotifyPropertyChanged
{
private UserControl _currentView = new DecisionMaker();
public UserControl CurrentView
{
get { return _currentView; }
set
{
_currentView = value;
OnPropertyChanged("CurrentView");
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
You need to implement INotifyPropertyChanged on MainWindowViewModel, such that the view is informed when the CurrentView property is changed.
It sounds like the behaviour you want is pretty much what you get with a [TabControl][1] - why not use this built in control and just bind the DataContext of both tabs to the same view model.
This also has the advantage that your view model wouldn't know about the view classes (I am assuming that UserControlMouse etc are user controls).
Note: this will not be applicable if you need the view model to be aware of whether it is in touch or mouse mode.
I have ContentPresenter with DataTemplateSelector:
...
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
var model = item as ItemControlViewModel;
if (model.CurrentStatus == PrerequisitesStatus.Required)
{
return RequiredTemplate;
}
if (model.CurrentStatus == PrerequisitesStatus.Completed)
{
return FinishedTemplate;
}
...
return InProgressTemplate;
}
When CurrentStatus is changed, OnPropertyChanged is called.
I need somehow to trigger this DataTemplateSelector when the property is changed and change ContentPresenter DataTemplate. Any suggestions?
Threre are similar questions:
1
2, but I don't want to use any DataTriggers, because of too much states.
Tried to play with DataTriggers
<ContentPresenter
Grid.Column="1"
Height="16"
Width="16"
Margin="3">
<ContentPresenter.Triggers>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="0">
<Setter Property="ContentPresenter.ContentTemplate" Value="{StaticResource ResourceKey=_requiredStatusTemplate}" />
</DataTrigger>
</ContentPresenter.Triggers>
</ContentPresenter>
But got an error:
Triggers collection members must be of type EventTrigger :(
As you requested an example with datatriggers in the comments, here you are:
A FrameworkElement can only have EventTriggers, therefore you get the error Message Triggers collection members must be of type EventTrigger
And also don't use a ContentPresenter directly, it is meant to be used inside a ControlTemplate. Better use a ContentControl when you want to have dynamic content.
See What's the difference between ContentControl and ContentPresenter?
And finally here's a suggestion to your DataTrigger issue. I have put it inside a style for reusability ....
XAML :
<Window x:Class="WpfApplication88.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">
<Window.Resources>
<DataTemplate x:Key="requiredTemplate">
<TextBlock Text="requiredTemplate"></TextBlock>
<!--your stuff here-->
</DataTemplate>
<DataTemplate x:Key="completedTemplate">
<TextBlock Text="CompletedTemplate"></TextBlock>
<!--your stuff here-->
</DataTemplate>
<Style x:Key="selectableContentStyle" TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="Required">
<Setter Property="ContentTemplate" Value="{StaticResource requiredTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="Completed">
<Setter Property="ContentTemplate" Value="{StaticResource completedTemplate}" />
</DataTrigger>
<!-- your other Status' here -->
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<ContentControl Width="100" Height="100" Style="{StaticResource selectableContentStyle}"/>
</Grid>
</Window>
I could be wrong, but I believe the DataTemplateSelector is only used when the ItemContainerGenerator creates a container for an item added to the collection. Because a new container isn't generated when a property value changes, a new DataTemplate is never going to be applied via the selector.
As suggested in the comments, I would recommend you look at the VisualStateManager or data triggers, otherwise you're going to have to recreate the container for every item when one or more properties change value.
Just as an extra choice - if you want to stick to your templates, just use s binding with converter.
I came up with a behavior that would theoretically do this.
C#:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;
public class UpdateTemplateBehavior : Behavior<ContentPresenter>
{
public static readonly DependencyProperty ContentProperty = DependencyProperty.Register(nameof(Content), typeof(object), typeof(UpdateTemplateBehavior), new FrameworkPropertyMetadata(null, OnContentChanged));
public object Content
{
get => GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
static void OnContentChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is UpdateTemplateBehavior behavior)
behavior.Update();
}
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register(nameof(Value), typeof(object), typeof(UpdateTemplateBehavior), new FrameworkPropertyMetadata(null, OnValueChanged));
public object Value
{
get => GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
static void OnValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
if (sender is UpdateTemplateBehavior behavior)
behavior.Update();
}
public UpdateTemplateBehavior() : base() { }
protected override void OnAttached()
{
base.OnAttached();
Update();
}
void Update()
{
if (Content != null)
{
BindingOperations.ClearBinding(AssociatedObject, ContentPresenter.ContentProperty);
AssociatedObject.Content = null;
BindingOperations.SetBinding(AssociatedObject, ContentPresenter.ContentProperty, new Binding() { Path = nameof(Content), Source = this });
}
}
}
XAML:
<ContentPresenter ContentTemplateSelector="{StaticResource MySelector}">
<i:Interaction.Behaviors>
<Behavior:UpdateTemplateBehavior Content="{Binding SomeContent}"
Value="{Binding SomeValue}"/>
</i:Interaction.Behaviors>
</ContentPresenter>
The content is "updated" (by clearing and then resetting the binding) when the content (in this example, "SomeContent") and an arbitrary value (in this example, "SomeValue") is changed, as well as when the behavior is first attached.
An update is not made unless the content is not null (my project-specific requirement). Not updating upon attaching may avoid unintentionally updating twice at once, but if the value is initially null, an update wouldn't occur until the value changes at least once.
Note: In the above example, I am not sure if the behavior has the same data context as the ContentPresenter. I use a helper class that I did not include here for brevity. Keep that in mind when testing...
I am having an issue when using a DataTrigger to manipulate the IsEnabled property of a control. Normally it works fine, however when I initialize the IsEnabled state within the View's Initialized event, the dynamic stylizing no longer works.
Here's my code. I trimmed it down to the simplest example I could.
Why is this occurring, and what can I do to allow me to set IsEnabled both by a style trigger and by initializing it in the code behind?
Thanks in advance!
View:
(Contains a textbox that should be enabled/disabled depending on the value of a checkbox)
<Window x:Class="IsEnabled.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Initialized="Window_Initialized">
<StackPanel Orientation="Vertical">
<TextBox x:Name="txtTarget" Width="200">
<TextBox.Style>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=ToggleValue}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
<CheckBox x:Name="chkSource" IsChecked="{Binding Path=ToggleValue}" />
</StackPanel>
</Window>
View Codebehind:
(The only addition is the implementation of the Initialized event setting the inital state for IsEnabled)
using System;
using System.Windows;
namespace IsEnabled.Views
{
public partial class MainView : Window
{
public MainView()
{
InitializeComponent();
}
private void Window_Initialized(object sender, EventArgs e)
{
txtTarget.IsEnabled = false;
}
}
}
ViewModel:
(ViewModelBase holds the implementation of the INotifyPropertyChanged interface)
using System;
namespace IsEnabled.ViewModels
{
class MainViewModel : ViewModelBase
{
private bool _ToggleValue;
public bool ToggleValue
{
get { return _ToggleValue; }
set
{
_ToggleValue = value;
OnPropertyChanged(this, "ToggleValue");
}
}
}
}
Have a look at dependency property value precedence, and how changing values from different places, Styles, Triggers, Animations etc. work together.
Add to your Binding Mode=TwoWay and it should work.