Bind to Datacontext property in button trigger? - wpf

I understand my problem however I'm looking for advice on a solution:
<Button.Style>
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource ButtonStyle}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="MouseOverControl" Value="True" />
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
I'm trying to bind to a property inside my datacontext, basically I want to tell the DC when a control in my UI has the mouse over it. I think I'll only need this for two buttons and it doesn't matter which one it's over, therefore I don't need to bother with a complicated solution (I hope).
Problem is at the moment it's looking for Button.MouseOverControl which obviously doesn't exist, I'd like to understand how you might go about accessing the DC instead.
Thanks!
EDIT: So I've attempted to go down the attached property/behavior route, here is what I have so far:
public static class MouseBehaviour
{
public static readonly DependencyProperty MouseOverProperty
= DependencyProperty.RegisterAttached(
"MouseOver",
typeof(bool),
typeof(MouseBehaviour),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
MouseOverBindingPropertyChanged));
public static bool GetMouseOver(DependencyObject obj)
{
return (bool)obj.GetValue(MouseOverProperty);
}
public static void SetMouseOver(DependencyObject obj, bool value)
{
obj.SetValue(MouseOverProperty, value);
}
private static void MouseOverBindingPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var element = d as FrameworkElement;
if (element != null)
{
// Unsure about this method..
}
}
}
Also I've added this to my button to try and link them, it appears to work:
ex:MouseBehaviour.MouseOver="{Binding MouseOverControl}"
However nothing happens, this is because I think it's working the wrong way around at the moment, so it is expecting my DC property to change but I want it so the MouseOverControl in my DC reflects the value of the IsMouseOver property of my button. Would it be as simple as:
SetMouseOver(element, element.IsMouseOver);
Or something similar?

First thing come to mind is binding IsMouseOver property to MouseOverControl property in viewmodel directly without trigger. Unfortunately that scenario is not supported.
One possible workaround to address that limitation is using event that is raised whenever IsMouseOver property changed to trigger method/command in viewmodel. We can do that using interaction triggers. Since IsMouseOverChanged event doesn't exist, we can use 2 events (MouseEnter and MouseLeave) as alternative.
<Button>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseEnter">
<ei:CallMethodAction MethodName="MouseEnter" TargetObject="{Binding}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeave">
<ei:CallMethodAction MethodName="MouseLeave" TargetObject="{Binding}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
Then have corresponding methods in viewmodel :
public void MouseEnter()
{
MouseOverControl = true;
}
public void MouseLeave()
{
MouseOverControl = false;
}
Another possible way is by creating attached behavior for MouseOver so that you can bind it to viewmodel's property as demonstrated in this blog post.

So I ended up solving this by adding my own action to update a property because CallMethodAction is only available in Blend 4 which at the time I'm unable to use.
This question helped me considerably: Setting a property with an EventTrigger
In particular I'd like to direct you to user Neutrino's answer on that page (Here), the only part I needed to change was the XAML implementation:
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseEnter">
<ex:SetPropertyAction PropertyName="MouseOverControl" TargetObject="{Binding}"
PropertyValue="true" />
</i:EventTrigger>
<i:EventTrigger EventName="MouseLeave">
<ex:SetPropertyAction PropertyName="MouseOverControl" TargetObject="{Binding}"
PropertyValue="false"/>
</i:EventTrigger>
</i:Interaction.Triggers>
Quick explanation is whenever the mouse enters a button I've added these triggers to, it sets a property in my viewmodel/datacontext to mirror this, perfect! Credit to har07 for providing several alternate solutions which also would have worked in different situations (and if I could figure out attached behaviors!!)

Related

MVVM: run view-only code after calling command

What I have:
using MVVM pattern
a view written in XAML
a command MyCommand in the ViewModel which gets called from several places in the view
a method DoSthInView that operates on the view, defined in codebehind
My Goal:
Whenever the command is executed, I want to call DoSthInView, no matter which control executed the command.
Question:
Since in MVVM the ViewModel does not know the View, I cannot call DoSthInView from the ViewModel. So how do call this code?
Own thoughts:
To be less abstract, this is my use case: We have one Button and one TextBox. The command takes the text which is currently in the TextBox and writes it somewhere into the model data. When this writing is done, I want to animate a green checkmark appearing and fading out (this is DoSthInView), so that the user gets a visual confirmation that the data was updated.
There are two ways of running the command:
Click the Button
Press "Enter" while the TextBox is focused
For the Button I know a way to call DoSthInView:
<Button Content="run command" Command="{Binding MyCommand}" Click={Binding DoSthInView}" />
For the TextBox, I have a KeyBinding to take the Enter key:
<TextBox>
<TextBox.InputBindings>
<KeyBinding Command="{Binding MyCommand}" Key="Enter" />
</TextBox.InputBindings>
</TextBox>
But InputBindings seem not to support events, only commands. So here I have no idea how to call DoSthInView.
But even if I found a way to call DoSthInView from within the input binding (analog to the Button), it wouldn't feel right. I am looking for a way to say "whenever MyCommand is executed, run DoSthInView" So that not every caller of MyCommand has to care for it individually, but there is just one place to handle that. Maybe this can be done in the root FrameworkElement?
What you are asking for is possible. You need to implement RelayCommand.
You can also see my other SO answer where there is an example.
Once you have RelayCommand implemented then you can do the following:
In ViewModel:
public ICommand MyCommand { get; set; }
public MyViewModel()
{
MyCommand = new RelayCommand(MyCommand_Execute);
}
private void MyCommand_Execute(object sender)
{
var myView = sender as MyView;
myView?.DoSthInView();
}
In View:
<TextBox>
<TextBox.InputBindings>
<KeyBinding Command="{Binding Path=MyCommand}" CommandParameter="{Binding}" Key="Enter"/>
</TextBox.InputBindings>
</TextBox>
While it is not recommended to mix view and viewModel, there can be scenarios where otherwise is not possible. and sometimes it can be requirements. But again this is NOT recommended.
While I am still interested in an answer to my original question (calling codebehind-code after a command is executed), Kirenenko's advice helped me to solve my actual problem regarding the animation. This answer doesn't fit the original question any more because there is no codebehind-code (the animation is solely written in XAML, leaving no codebehind-code to execute). I still put it here because it is partially useful for me.
In the ViewModel, I have this:
...
private bool _triggerBool;
public bool TriggerBool
{
get { return _triggerBool; }
set
{
if (_triggerBool != value)
{
_triggerBool = value;
NotifyPropertyChanged(nameof(TriggerBool));
}
}
}
...
public DelegateCommand MyCommand; // consists of MyCommandExecute and MyCommandCanExecute
...
public void MyCommandExecute()
{
... // actual command code
TriggerBool = true;
TriggerBool = false;
}
...
And here is the animation written in XAML and called by the DataTrigger:
<Image Source="myGreenCheckmark.png" Opacity="0">
<Image.Style>
<Style TargetType="Image">
<Style.Triggers>
<DataTrigger Binding="{Binding TriggerBool}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
From="1" To="0" Duration="0.0:0:0.750"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
Looks really stupid to set TriggerBool to true and then false again, but works...
Based on Leonid Malyshev's hint here is a quite clean solution using an event ("clean" regarding seperation of View and ViewModel):
ViewModel code:
public class MyViewModel
{
...
public event Action MyCommandExecuted;
public DelegateCommand MyCommand; // consists of MyCommandExecute, MyCommandCanExecute
...
private void MyCommandExecute()
{
... // actual command code
MyCommandExecuted.Invoke();
}
...
}
View Codebehind:
public partial class MyView : Window
{
public MyView(MyViewModel vm)
{
InitializeComponent();
DataConext = vm;
vm.MyCommandExecuted += DoSthInView();
}
...
private void DoSthInView()
{
...
}
...
}
Usually you have a better more MVVM-ish way to these this thing but since I don't know your case I'll assume that there is no other way.
So to this you need a dependency property in your view. A Boolean would be great and you handle it's on changed event and run DoSthInView in it. In your view model you set the value of this property and on changed gets called.
I can give you demonstration if you need. Also keep in mind that this is event driven coding which defiles MVVM. Try to use bindings and move your DoSthInView to ViewModel if possible.

wpf xaml resource dictionary add custom property

I want to use the following xaml code for navigation in some pages:
<Button Content="Go to page2">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:ChangePropertyAction PropertyName="Source" TargetObject="{Binding NavigationService, RelativeSource={RelativeSource AncestorType={x:Type Page}, Mode=FindAncestor}}">
<ei:ChangePropertyAction.Value>
<System:Uri>Page2.xaml</System:Uri>
</ei:ChangePropertyAction.Value>
</ei:ChangePropertyAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
So I´m wondering if there is a possibility to outsource the interaction part into a style (in a resource dictionary) and add a custom property like "NavigationUri" where you can directly declare the page to navigate to.
Another idea (which would probably be the better approach) is to create a custom control and inherit from button class.
Anywhere I would prefer a more compact and lean way without code behind.
Please let me know, which is the more suitable solution and how to implement it.
Although there are various simple techniques to make our Behavior Xaml as static resource. But, we need a custom behavior, as we are using a parameter in the form of Page name to navigate to. This variable demands programming.
So, I came up with
a. Custom behavior(NavigationBehavior), and
b. Button subclassing(NavigationButton)
NavigationBehavior
using System;
using System.Windows.Controls;
using System.Windows.Interactivity;
namespace WpfApplication1.Navigation
{
public class NavigationBehavior:Behavior<NavigationButton>
{
protected override void OnAttached()
{
AssociatedObject.Click += AssociatedObject_Click;
base.OnAttached();
}
void AssociatedObject_Click(object sender, System.Windows.RoutedEventArgs e)
{
((Page)AssociatedObject.DataContext).NavigationService.Source = new Uri(AssociatedObject.DestinationUri, UriKind.Relative);
}
}
}
NavigationButton
namespace WpfApplication1.Navigation
{
public class NavigationButton : Button
{
NavigationBehavior behavior = new NavigationBehavior();
public NavigationButton()
{
behavior.Attach(this);
}
public string DestinationUri { get; set; }
}
}
Usage :
<nav:NavigationButton Content="Navigate to Page2" DestinationUri="/Navigation/Page2.xaml" />
Important Note
We are using DataContext property in our behavior to get access to the containing page. So, set this.DataContext = this; in the constructor of your all pages.
One can try using a common base class / interface to avoid this.
you cannot have interaction triggers extracted but there is a workaround to get this done
you can create a Button in resources with interaction logic in it, and then where needed you can have a content control with content set to your resource.
something like this --
<Window.Resources>
<Button x:Key="MyButton"
Content="Go to page2">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<ei:ChangePropertyAction PropertyName="Source" TargetObject="{Binding NavigationService, RelativeSource={RelativeSource AncestorType={x:Type Page}, Mode=FindAncestor}}">
<ei:ChangePropertyAction.Value>
<System:Uri>Page2.xaml</System:Uri>
</ei:ChangePropertyAction.Value>
</ei:ChangePropertyAction>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Window.Resources>
<Grid>
<ContentControl Name="MyLocalButton" Content="{StaticResource MyButton}" />
</Grid>
You should put the navigation logic in a command in each page's view model and bind the buttons' Command properties to those commands, or else give the view models each a NextPage property and bind to that.
And create separate buttons in the views. Define a Style in the resource dictionary to make them all look the same.
Defining a Button as a resource is a bad idea. Among other things, there's only one instance of it and it can have only one visual parent, so when you add it to one view it'll vanish from the last. And you run into ugly problems like this one. You're working against XAML, and XAML is already hard enough when you're working with it.

Filter clicks on Column Header

I am using a construct as described in this earlier question of mine, which looks something like
<ListView x:Name="listView">
<ListView.View>
<GridView />
</ListView.View>
<ListView.Resources>
<Style TargetType="{x:Type ListViewItem}"/>
</ListView.Resources>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseRightButtonUp">
<i:InvokeCommandAction Command="{Binding RightClickOnItemCommand}"
CommandParameter={Binding SelectedItem,
ElementName=listView} />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListView>
This works like a charm, except it fires when I right-click on the column headers. Since I only get the SelectedItem as a parameter for the command and the selected item property doesn't clear when you click a header (and I don't want it to, either), I don't see a way to fix this problem on the viewmodel side.
Until now, I really liked this solution, since it provides a really nice and clean way to handle those events, but this corner case drives me crazy.
I know I could add an event setter to the ListViewItem Style, but the handler would then in turn require me to write code-behind, which I wanted to avoid in the first place, hence the System.Windows.Interactivity-Stuff.
Is there an equally nice and clean way to prevent that from happening (i.e. a way that does not involve me writing ugly code-behind hacks)?
The problem can be solved with the method described here. The class that is developed in the article essentially adds an attached property to hold the triggers.
public static readonly DependencyProperty TemplateProperty =
DependencyProperty.RegisterAttached("Template",
typeof(InteractivityTemplate),
typeof(InteractivityItems),
new PropertyMetadata(default(InteractivityTemplate), OnTemplateChanged));
private static void OnTemplateChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
InteractivityTemplate dt = (InteractivityTemplate)e.NewValue;
dt.Seal();
InteractivityItems ih = (InteractivityItems)dt.LoadContent();
BehaviorCollection bc = Interaction.GetBehaviors(d);
TriggerCollection tc = Interaction.GetTriggers(d);
foreach (Behavior behavior in ih.Behaviors)
bc.Add(behavior);
foreach (TriggerBase trigger in ih.Triggers)
tc.Add(trigger);
}
This now allows to add triggers to the individual items:
<ListView.Resources>
<Style TargetType="{x:Type ListViewItem}">
<Setter Property="int:InteractivityItems.Template">
<Setter.Value>
<int:InteractivityTemplate>
<int:InteractivityItems>
<int:InteractivityItems.Triggers>
<i:EventTrigger EventName="MouseRightButtonUp">
<i:InvokeCommandAction
Command="{Binding RightClickCommand}" />
</i:EventTrigger>
</int:InteractivityItems.Triggers>
</int:InteractivityItems>
</int:InteractivityTemplate>
</Setter.Value>
</Setter>
</Style>
</ListView.Resources>
This actually models the idea of clicking an item instead of clicking on the list and finding the item that was below the pointer when the list was clicked.

Changing Style via data binding depending on CheckBox state

In one of my WPF user controls I have a check box. If the check box is not checked, I would like to use the following:
<vf:DataSeries Style="{StaticResource dataSeriesQuickLine}" ... >
However, if it is checked, I would like to use the following:
<vf:DataSeries Style="{StaticResource dataSeriesLine}" ... >
Is there a way I can bind the Style to the check box control to use the styling I want?
Thanks.
Yes, you could bind to IsChecked and use a Binding.Converter which has properties for the styles and returns either depending on the input value.
You could use a generic boolean converter:
<vc:BooleanConverter x:Key="StyleConverter"
TrueValue="{StaticResource Style1}"
FalseValue="{StaticResource Style2}"/>
public class BooleanConverter : IValueConverter
{
public object TrueValue { get; set; }
public object FalseValue { get; set; }
// In Convert cast the value to bool and return the right property
}
Add following namespaces to your xaml:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
Set the default style on your control to Style2. Then
assign a name to your control and
add following trigger and action somewhere in your xaml (e.g. before you close your vf:DataSeries tag):
<i:Interaction.Triggers>
<ei:DataTrigger
Binding="{Binding ElementName=yourCheckboxName, Path=IsChecked}"
Value="True">
<ei:ChangePropertyAction TargetName="yourControlName"
PropertyName="Style"
Value="{StaticResource Style1}"/>
</ei:DataTrigger>
</i:Interaction.Triggers>

How to select ListBoxItem upon clicking on button in Template?

I have the following Data Template applied to a ListBox:
<DataTemplate x:Key="MyTemplate" DataType="{x:Type DAL:Person}">
<StackPanel Orientation="Horizontal">
<Button Content="X" Command="{x:Static cmd:MyCommands.Remove}"/>
<TextBlock Text="{Binding Person.FullName}" />
</StackPanel>
</DataTemplate>
When I click on the button the command gets fired but the ListBoxItem doesn't get selected. How do I force it to get selected, so that I can get the selected item in my "executed" method?
Thanks
A better way, since you're not really interested in selecting the item (because it will quickly get deleted anyway) would be to pass the item itself to the Command as a CommandParameter.
Alternatively, you can go about in a roundabout manner either with code-behind or with triggers, but I don't think it would be as to the point. For example:
you could handle the ButtonBase.Click event on your listbox, like
<ListBox ButtonBase.Click="lb_Click"
...
then in your code behind, do this:
private void lb_Click(object sender, RoutedEventArgs e)
{
object clicked = (e.OriginalSource as FrameworkElement).DataContext;
var lbi = lb.ItemContainerGenerator.ContainerFromItem(clicked) as ListBoxItem;
lbi.IsSelected = true;
}
That gets the clicked bound item, because the datacontext of the button is inherited from it's templated item, then the actual autogenerated ListBoxItem from the ListBox's ItemContainerGenerator, and sets the IsSelected property to true. I think that's one of the fastest and easiest ways. Also works with multiple ButtonBase-derived objects in the template.
Of course you can also more nicely encapsulate all this (more or less exactly the same) as a reusable Behavior:
public class SelectItemOnButtonClick : Behavior<ListBox>
{
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.AddHandler(ButtonBase.ClickEvent, new RoutedEventHandler(handler), true);
}
protected override void OnDetaching()
{
this.AssociatedObject.RemoveHandler(ButtonBase.ClickEvent, new RoutedEventHandler(handler));
base.OnDetaching();
}
private void handler(object s, RoutedEventArgs e)
{
object clicked = (e.OriginalSource as FrameworkElement).DataContext;
var lbi = AssociatedObject.ItemContainerGenerator.ContainerFromItem(clicked) as ListBoxItem;
lbi.IsSelected = true;
}
}
You can use it like this:
<ListBox xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" ...>
<i:Interaction.Behaviors>
<local:SelectItemOnButtonClick />
</i:Interaction.Behaviors>
</ListBox>
Add error handling code like at least null checks, of course - wouldn't want a simple thing like this bombing your app.
To understand the problem, the button sets the Handled property to true for all the mouse events that act on it (MouseDown/Click) so they aren't being considered by the ListBoxItem. You could also attach the MouseDown event to the ListBox and walk the visual tree upwards until you reach the parent ListBoxItem but that's a lot more tricky... eh if you're curious, you can read this article to know why, basically you'll also encounter FrameworkContentElements (which also respond to MouseDown) so the code will get more complicated, with the upside that anything clicked inside the datatemplate will trigger the ListBoxItem to be selected, regardless of whether it marked the event as handled.
Heh, I also tried to do it exclusively with styles and triggers but it got ugly fast and I lost interest (and lost track of all the... err thingies). Basically it could be solved, I think, but I reaaaly don't think it's worth the bother. Maybe I overlooked something obvious though, don't know.
Make the underlying object expose a RemoveCommand property, and bind the button's Command property to it. This simplifies the data template; it also greatly simplifies the case where application logic may dictate that a specific item can't be removed.
Alex, thanks for answer. Your solution with Behavior is great. First solution is not so good because that will work only if you click on specific Button. Here is one more solution that will work on click on arbitrary control form ListBoxItem template:
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem"
BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Style.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="True">
<Setter Property="IsSelected" Value="True"/>
</Trigger>
</Style.Triggers>
</Style>
</ListBox.ItemContainerStyle>
That is XAML only approach. I also set BasedOn property just to be sure to not override the current ListBoxItem style.

Resources