I am trying to set the color of an SVG in a toggle button whenever the forecolor of the button is changed (e.g. on hover or button pressed). The interface of the SVGImage class requires to wrap the brush into a dictionary and assign it to it's CustomBrushes property. I have written a value converter for this. However, the Convert method of this converter is not called on events like mouse hovering or if the button is pressed although the text below the image changes it's color.
As I understand I have to set the relative source for the binding like this:
<ToggleButton Style="{StaticResource MyToggleButtonStyle}">
<StackPanel>
<svg:SVGImage Source="/CommonResources;component/Svg/MyIcon.svg"
CustomBrushes="{Binding
Path=Foreground,
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ToggleButton}},
Converter={StaticResource BrushToDictionaryConverter}}" />
<TextBlock>Button Caption</TextBlock>
</StackPanel>
</ToggleButton>
For completeness although the problem should be in the first snippet. The style looks like this:
<Style x:Key="MyToggleButtonStyle" TargetType="ToggleButton">
<Setter Property="Foreground" Value="White" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ToggleButton}">
<Border Background="{TemplateBinding Background}">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="Yellow" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Foreground" Value="Red" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Foreground" Value="Green" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsChecked" Value="True"/>
<Condition Property="IsPressed" Value="True"/>
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Foreground" Value="Red" />
</MultiTrigger.Setters>
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
and the converter like this:
/// <summary>
/// Converter to wrap a color into a dictionary.
/// </summary>
public class BrushToDictionaryConverter : IValueConverter
{
/// <summary>
/// Returns a dictionary containing a brush as value and
/// a string representation of the brush as key.
/// </summary>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is SolidColorBrush scb)
return new Dictionary<string, Brush> { { scb.Color.ToString(), scb } };
else if (value is Brush b)
return new Dictionary<string, Brush> { { "custom", b } };
return new Dictionary<string, Brush> { { "empty", new SolidColorBrush(Colors.Transparent) } };
}
/// <summary>
/// Returns a brush from the dictionary assuming the provided value is a dictionary containing
/// brushes as values.
/// </summary>
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Dictionary<string, Brush> dict && dict.Any())
return dict.First().Value;
return new SolidColorBrush(Colors.Transparent);
}
}
Does anyone see why the Convert method is not even called although the style is working and the button's caption changes it's color on user interaction?
I found the solution after I added diag:PresentationTraceSources.TraceLevel=High to the binding which gave me this output:
System.Windows.Data Warning: 61 : BindingExpression (hash=14000148): Default update trigger resolved to PropertyChanged
System.Windows.Data Warning: 62 : BindingExpression (hash=14000148): Attach to SVGImage.SVG.SVGImage.CustomBrushes (hash=57434139)
System.Windows.Data Warning: 66 : BindingExpression (hash=14000148): RelativeSource (FindAncestor) requires tree context
System.Windows.Data Warning: 65 : BindingExpression (hash=14000148): Resolve source deferred
System.Windows.Data Warning: 79 : BindingExpression (hash=14000148): Deactivate
System.Windows.Data Warning: 63 : BindingExpression (hash=14000148): Detach
Finally, I also added the source code of the DotnetProjects.SVGImage nuget package it turned out to be a glitch in the SVGImage's OnInitialized method. At least it is working if I comment the last line of that method (which probably means that I have broken something else but for me it works.)
Related
My idea is to use a WPF default button use an image as the opacity mask and have the foreground brush change its color. I want to still have the background however and this is where the issue is, because no matter how I design the style the background always disappears. Either this isn’t possible because the opacity Mask cannot change its target and just acts on the entire control or I am missing something.
Here is the style i have. I am trying to keep it fairly basic.
<Style x:Key="ButtonStyle" TargetType="Button">
<Setter Property="OverridesDefaultStyle" Value="True"/>
<Setter Property="Background" Value="{DynamicResource Mono_Darkest}"/>
<Setter Property="Foreground" Value="{DynamicResource Mono_Light}"/>
<Setter Property="BorderBrush" Value="{StaticResource Button_BorderBrush_Normal}"/>
<Setter Property="BorderThickness" Value="{StaticResource Button_BorderThickness_Normal}"/>
<Setter Property="FontFamily" Value="{StaticResource Button_FontFamily_Normal}"/>
<Setter Property="FontWeight" Value="{StaticResource Button_FontWeight_Normal}"/>
<Setter Property="FontSize" Value="{StaticResource Button_FontSize_Normal}"/>
<Setter Property="HorizontalContentAlignment" Value="{StaticResource Button_HorizontalContentAlignment_Normal}"/>
<Setter Property="VerticalContentAlignment" Value="{StaticResource Button_VerticalContentAlignment_Normal}"/>
<Setter Property="Padding" Value="{StaticResource Button_Padding_Normal}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="Border_Part"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Label OpacityMask="{TemplateBinding OpacityMask}"
Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}" FontFamily="{TemplateBinding FontFamily}"
FontSize="{TemplateBinding FontSize}" FontWeight="{TemplateBinding FontWeight}">
<Label.Style>
<Style TargetType="Label">
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=OpacityMask, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}, Converter={StaticResource isNull}}"
Value="false">
<Setter Property="Background"
Value="{Binding Path=Foreground, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Button}}}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
What I have is simply nesting a label inside the border. What I want is the template (Button) control's opacity property to only affect the label's opacity property. But that doesn’t seem to be happening. Instead it seems to affect everything like the default button style would.
I have tried Setting OverridesDefaultStyle to True but that has not changed anything.
I know I can already do this other ways. But my goal is to just make a quick button that can use either text or images as a foreground. With the image color coming from the foreground brush. If I cannot get an answer here I am most likely just going to use an Attached Property and bind the labels opacity mask to that instead, but again I would rather just do it entirely in this style.
Any advice is welcome. Thank you.
For Opacity I use a Converter. With this I can bind a SolidColorBrush to Foreground or Background. The Converter gets a Parameter with the target Opacity and returns the opac color.
Converter:
public class ColorToOpacityConverter : IValueConverter
{
// Takes a SolidColorBrush as value and double as parameter
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is SolidColorBrush)) return value;
SolidColorBrush scb = new SolidColorBrush(((SolidColorBrush) value).Color);
scb.Opacity = parameter as double? ?? 0.5;
return scb;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
xaml:
<Style.Resources>
<namespace:ColorToOpacityConverter x:Key="ColorToOpacityConverter" />
<system:Double x:Key="TargetOpacity">0.3</system:Double>
</Style.Resources>
<Label Foreground="{Binding Path=Foreground, StaticResource={StaticResource TemplatedParent}, Converter={StaticResource ColorToOpacityConverter}, ConverterParameter={StaticResource TargetOpacity}"
A second approach for bound opacities (not as StaticResource) I use a MultiValueConverter
For bound opacities:
public class ColorToDynOpacityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Any(v => v == DependencyProperty.UnsetValue)) return new SolidColorBrush();
if (!(values[0] is SolidColorBrush) || !(values[1] is double)) {
//Fake for DesignMode-Display
if (DesignerProperties.GetIsInDesignMode(new DependencyObject())) return new SolidColorBrush(Colors.Aqua);
throw new ArgumentException("expect values[0] as SolidColorBrush and values[1] as double.");
}
SolidColorBrush scb = new SolidColorBrush(((SolidColorBrush) values[0]).Color);
scb.Opacity = (double) values[1];
return scb;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
xaml:
<namespace:ColorToDynOpacityConverter x:Key="ColorToDynOpacityConverter" />
<Label>
<Label.Foreground>
<MultiBinding Converter="{StaticResource ColorToDynOpacityConverter}">
<Binding Path="Foreground" RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Path="Opacity" RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</Label.Foreground>
</Label>
Hope that helps and working for you.
We have a control that may or may not be hosted in a popup control. In the case when it is, we want to set properties on the popup using RelativeSource and OneWayToSource bindings. In cases where it's not hosted in a popup, we want the binding to basically be ignored/do nothing.
Only thing I can think of is binding to self with a custom converter which internally walks the visual tree looking for the popup. If found, do the magic. If not, do nothing. But I'm wondering if it can be done purely with XAML binding syntax.
In cases where it's not hosted in a popup, we want the binding to basically be ignored/do nothing.
Since you have a control one can create a Boolean dependency property, a flag, which can trigger either one of two hidden controls which behaves in a specific way due to which way the boolean is set.
I would call this the standard way, for the control is not required to know anything about the consumer, the consumer specifies the state.
Or
to set properties on the popup using RelativeSource and OneWayToSource bindings.
Similar to above, with two distinct hidden controls but then have the a style look for a specific window and a specific property. Then either hide or show the controls depending on what is found:
<Setter Property="IsEnabled" Value="False" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsPopup,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Page}}}"
Value="True">
<Setter Property="IsEnabled"
Value="True" />
</DataTrigger>
</Style.Triggers>
Try following code:
<Border>
<TextBlock>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="Exists"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=StackPanel}}" Value="{x:Null}">
<Setter Property="Text" Value="No stackpanel"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Border>
It will display both "No stackpanel" and error in Output window. If you place StackPanel inside Border "Exists" will be displayed. Set anything you wish inside DataTrigger when condition is fulfilled.
In case you want to avoid receiving error:
<Window.Resources>
<local:IsParentTypePresentToBoolConverter x:Key="IsParentTypePresentToBoolConverter"/>
</Window.Resources>
<Border>
<TextBlock>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="No StackPanel"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource IsParentTypePresentToBoolConverter}}" Value="True">
<Setter Property="Text" Value="Stackpanel exists as parent"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Border>
converter which detects whether such a type is present as a parent:
class IsParentTypePresentToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var isPresent = FindParent<StackPanel>((DependencyObject) value);
return isPresent != null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
private T FindParent<T>(DependencyObject child) where T : DependencyObject
{
var parentObject = VisualTreeHelper.GetParent(child);
if (parentObject == null) return null;
var parent = parentObject as T;
return parent?? FindParent<T>(parentObject);
}
}
And here you have more generic equivalent where you make a use of reflection in order to find parent type.
class IsParentTypePresentToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
var method = GetType().GetMethod("FindParent").MakeGenericMethod(new Type[1] { (Type)parameter });
var foundObject = method.Invoke(this, new object[] { (DependencyObject)value });
return foundObject != null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
public T FindParent<T>(DependencyObject child) where T : DependencyObject
{
var parentObject = VisualTreeHelper.GetParent(child);
if (parentObject == null) return null;
var parent = parentObject as T;
return parent ?? FindParent<T>(parentObject);
}
}
The only distinction in XAML is that you indicate searching type of object.
<Border>
<TextBlock>
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="No StackPanel"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource IsParentTypePresentToBoolConverter},
ConverterParameter={x:Type Border}}" Value="True">
<Setter Property="Text" Value="Stackpanel exists as parent"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Border>
I have what I hope is an obvious problem to someone. I want an Edit command to fire when a user double clicks a ListBoxItem inside a ListBox. I have done this before in user controls but want to do it directly in a VIEW since it is a simple enough ListBox. But it will not wire up.
Here is the list box:
<ListBox SelectedItem="{Binding DataQuerySortSelected}"
ItemContainerStyle="{StaticResource SortListItemStyle}"
ItemsSource="{Binding DataQueryHolder.DataQuerySorts}">
<ListBox.InputBindings>
<KeyBinding Key="Delete" Command="{Binding DataQuerySortDelete}" />
</ListBox.InputBindings>
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="AllowDrop" Value="True" />
<EventSetter Event="PreviewMouseMove" Handler="DragDropListBoxItem_PreviewMouseMoveEvent" />
<EventSetter Event="Drop" Handler="DragDropListBoxItem_Drop" />
</Style>
</ListBox.Resources>
</ListBox>
Note the Delete Key binding at the top level works just fine. Here is the referenced style (brought in as a separate ResourceDictionary but putting the style inline made no difference):
<Style x:Key="SortListItemStyle" TargetType="ListBoxItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Name="MainBorder">
<ContentPresenter>
<ContentPresenter.InputBindings>
<MouseBinding Gesture="LeftDoubleClick" Command="{Binding DataQuerySortEdit}" />
</ContentPresenter.InputBindings>
</ContentPresenter>
<Border.InputBindings>
<MouseBinding Gesture="LeftDoubleClick" Command="{Binding DataQuerySortEdit}" />
</Border.InputBindings>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="MainBorder" Value="Yellow" Property="Background" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I put the mouse binding in two places just to see if it made a difference but it does not. There just isnt any wiring going on there. Everything else works as expected. If I create a plain button in the View and point it at the DataQuerySortEdit Command it works as expected.
Am I missing something? Thanks for any help.
EDIT: Just adding some more info in response to J's response. I gave the Border of the ControlTemplate a binding relative to the nearest listbox and gave the listbox a name so the output would confirm it would find it. This was the output window:
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''DataQuerySort' (HashCode=7641038)'. BindingExpression:Path=DataQuerySortEdit; DataItem='DataQuerySort' (HashCode=7641038); target element is 'MouseBinding' (HashCode=65119131); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''DataQuerySort' (HashCode=50439840)'. BindingExpression:Path=DataQuerySortEdit; DataItem='DataQuerySort' (HashCode=50439840); target element is 'MouseBinding' (HashCode=3649016); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''DataQuerySort' (HashCode=65588106)'. BindingExpression:Path=DataQuerySortEdit; DataItem='DataQuerySort' (HashCode=65588106); target element is 'MouseBinding' (HashCode=35717517); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''DataQuerySort' (HashCode=32836053)'. BindingExpression:Path=DataQuerySortEdit; DataItem='DataQuerySort' (HashCode=32836053); target element is 'MouseBinding' (HashCode=66172851); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''ListBox' (Name='SortListBox')'. BindingExpression:Path=DataQuerySortEdit; DataItem='ListBox' (Name='SortListBox'); target element is 'MouseBinding' (HashCode=28263486); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''ListBox' (Name='SortListBox')'. BindingExpression:Path=DataQuerySortEdit; DataItem='ListBox' (Name='SortListBox'); target element is 'MouseBinding' (HashCode=27134857); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''ListBox' (Name='SortListBox')'. BindingExpression:Path=DataQuerySortEdit; DataItem='ListBox' (Name='SortListBox'); target element is 'MouseBinding' (HashCode=7437765); target property is 'Command' (type 'ICommand')
System.Windows.Data Error: 40 : BindingExpression path error: 'DataQuerySortEdit' property not found on 'object' ''ListBox' (Name='SortListBox')'. BindingExpression:Path=DataQuerySortEdit; DataItem='ListBox' (Name='SortListBox'); target element is 'MouseBinding' (HashCode=58400697); target property is 'Command' (type 'ICommand')
So the second attempt to bind (I am guessing the one in the Border InputBinding) does fine the proper listbox but still cannot find the ICommand. I tried doing a relative find to the window, to the Gird containing the list, etc. and still cant get it to wire. I also tried as J mentions to put the relative search directly in the MouseBindings and they result in the same errors.
EDIT2: Here are the Command and properties in the ViewModel, using MVVMLight
public DataQuerySort DataQuerySortSelected
{
get { return _DataQuerySortSelected; }
set { NotifySetProperty(ref _DataQuerySortSelected, value, () => DataQuerySortSelected); }
}
private DataQuerySort _DataQuerySortSelected;
public RelayCommand DataQuerySortEdit
{
get { return new RelayCommand(_DataQuerySortEdit, CanDataQuerySortEdit); }
}
private void _DataQuerySortEdit()
{
DataQuerySortHolder = DataQuerySortSelected.Copy();
DataQuerySortEditMode = EditMode.Edit;
}
private bool CanDataQuerySortEdit()
{
return DataQuerySortSelected != null;
}
Taking out the CanDataQuerySortEdit makes not difference. I know everything works because if I create a button and point at it it works. If I also create an inputbinding in the ListBox for the mouse like the Delete key that works - as long as I click outside the ListBoxItems of course.
EDIT3: Here is Part of the View itself including the class, datacontext, and resources. I have tried doing relative bindings to be "{x:Type Window}" and "{x:Type l:ToolkitWindowBase}". The ToolkitWindowBase extends Window directly. The frmDataBrowserViewModel extends a class called ToolkitViewModelBase which extends ViewModelBase from MVVMLight:
<l:ToolkitWindowBase x:Class="GISToolkit.frmDataBrowser" x:Name="mainWindow" Icon="Images/favicon.ico"
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" mc:Ignorable="d"
xmlns:l="clr-namespace:GISToolkit;assembly="
xmlns:lc="clr-namespace:GISToolkit.Controls;assembly="
xmlns:ls="clr-namespace:GISToolkit.Settings;assembly="
xmlns:system="clr-namespace:System;assembly=mscorlib"
xmlns:xctk="clr-namespace:Xceed.Wpf.Toolkit;assembly=Xceed.Wpf.Toolkit"
xmlns:xctkp="clr-namespace:Xceed.Wpf.Toolkit.Primitives;assembly=Xceed.Wpf.Toolkit"
Title="Solutions GIS Toolkit - Setup"
ResizeMode="CanResizeWithGrip" Foreground="White"
l:ControlBox.HasMaximizeButton="False" l:ControlBox.HasMinimizeButton="False" l:ControlBox.HasCloseButton="False"
Height="{Binding RunTimeHeight, Mode=TwoWay}"
Width="{Binding RunTimeWidth, Mode=TwoWay}" IsSettingsDirty="{Binding IsCurrentSettingsDirty}" IsEnabled="True">
<Window.DataContext>
<l:frmDataBrowserViewModel />
</Window.DataContext>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Themes/DataBrowser.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
..................
<l:ToolkitWindowBase />
EDIT4: Just in case someone out there is still listing, do me a favor, create a new WPF project called "WpfMvvmApplication1" with a single Window called "BindingTestWindow" and a viewmodel called "MainWindowViewModel" Then for the window put (should be simple cut/paste unless you used different names for the files/project):
<Window x:Class="WpfMvvmApplication1.BindingTestWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:WpfMvvmApplication1"
Title="BindingTestWindow" Height="300" Width="300">
<Window.DataContext>
<l:BindingTestViewModel />
</Window.DataContext>
<Grid>
<GroupBox Header="Sorting:" >
<Grid>
<ListBox Background="White" Name="SortListBox" ItemsSource="{Binding TestCollection}">
<ListBox.InputBindings>
<KeyBinding Key="Delete" Command="{Binding TestCommand}" />
</ListBox.InputBindings>
<ListBox.Resources>
<Style TargetType="{x:Type ListBoxItem}">
<Setter Property="AllowDrop" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Name="MainBorder" Padding="0" Margin="0">
<ContentPresenter />
<Border.InputBindings>
<MouseBinding Gesture="LeftDoubleClick" Command="{Binding TestCommand, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
</Border.InputBindings>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="MainBorder" Value="Yellow" Property="Background" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListBox.Resources>
</ListBox>
</Grid>
</GroupBox>
</Grid>
</Window>
and for the VIEWMODEL:
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Windows.Input;
namespace WpfMvvmApplication1
{
public class BindingTestViewModel : NotificationObject
{
public BindingTestViewModel()
{
TestCollection = new ObservableCollection<string>();
for (int i = 0; i < 10; i++ )
TestCollection.Add("test" + i);
}
public ICommand TestCommand { get { return new DelegateCommand(_TestCommand); } }
private void _TestCommand() { System.Diagnostics.Debugger.Break(); }
public ObservableCollection<string> TestCollection
{
get { return _TestCollection; }
set
{
_TestCollection = value;
RaisePropertyChanged(() => TestCollection);
}
}
private ObservableCollection<string> _TestCollection;
}
public class DelegateCommand : ICommand
{
private readonly Action _command;
private readonly Func<bool> _canExecute;
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
public DelegateCommand(Action command, Func<bool> canExecute = null)
{
if (command == null)
throw new ArgumentNullException();
_canExecute = canExecute;
_command = command;
}
public void Execute(object parameter)
{
_command();
}
public bool CanExecute(object parameter)
{
return _canExecute == null || _canExecute();
}
}
public class NotificationObject : INotifyPropertyChanged
{
protected void RaisePropertyChanged<T>(Expression<Func<T>> action)
{
var propertyName = GetPropertyName(action);
RaisePropertyChanged(propertyName);
}
private static string GetPropertyName<T>(Expression<Func<T>> action)
{
var expression = (MemberExpression)action.Body;
var propertyName = expression.Member.Name;
return propertyName;
}
private void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
}
This has nothing else in it. It gives me the binding error when the inputbinding is inside the listboxtiem but not when in, say, the listbox itself. Seems like it should work since the output says it does find the window in the FindAncestor.
Try changing your command binding to the following code. Listbox item is a visual child of listbox and you need to get your datacontext wired up correctly. I think. Note the Window Type. Change that to whatever your top level datacontext is where the command is declared and the listbox exists. Ie: usercontrol, window, etc...
<MouseBinding Gesture="LeftDoubleClick" Command="{Binding DataQuerySortEdit, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}}" />
You might be able to debug this my looking at your output window when you run your app, it will show you binding errors like this.
EDIT 1: Mvvm Light Event to Command
Okay, I totally overlooked the fact that you are suing Mvvm Light. MVVM-Light has a function built in for binding events to commands on your view model.
Add the following to your windows namespaces:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="http://www.galasoft.ch/mvvmlight"
Now change your border inside your listbox item to include the following:
<Border ...>
<i:Interaction.Triggers>
<i:EventTrigger EventName="LeftDoubleClick">
<cmd:EventToCommand Command="{Binding DataContext.DataQuerySortEdit, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Border>
Mvvm light has built this in order to facilitate exactly what you are looking for. It lets you take any event and bind a command to it. I use this for form validation through the lostfocus events of controls etc.
Make sure you have the following references in your project:
GalaSoft.MvvmLight.WPF45
GalaSoft.MvvmLight.Extras.WPF45
Microsoft.Expression.Interactions
Hope this helps
EDIT 3:
Just a last ditch effort, what if you use the following:
It adds the Datacontext to the command name.
Big thanks to J King for hanging in there. But it seems that doing in XAML does not work. I ended up doing this in the code behind of the VIEW (hope it can help someone):
public BindingTestWindow()
{
InitializeComponent();
SortListBox.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}
private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
if (SortListBox.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
{
BindingTestViewModel vm = (BindingTestViewModel)this.DataContext;
for(int i = 0; i < SortListBox.Items.Count; i++)
{
ListBoxItem lbi = (ListBoxItem)SortListBox.ItemContainerGenerator.ContainerFromIndex(i);
lbi.InputBindings.Clear();
lbi.InputBindings.Add(new InputBinding(vm.TestCommand, new MouseGesture(MouseAction.LeftDoubleClick)));
}
}
}
In order to manipulate the actual listboxitems (and not their content through ListBox.Items) we have to use the ItemContainerGenerator. But in order to do that, you have to wait for it to be completely generated hence the need for the event handler. Its not pretty but it works.
Ernie
I spent quite a bit of time fiddling with this problem. Managed to mix some answers together with a result that's working.
Inputbindings on a ListBox have a strange behavior. Some work on the ListBox itself, like a MiddleClick, others need to be implemented on the items.
J Kings Binding to the DataContext did the trick in the ContentPresenter of the LisBoxItemStyle.
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ListBoxItem}">
<Border x:Name="Bd"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="true">
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<ContentPresenter.InputBindings>
<MouseBinding Gesture="Ctrl+MiddleClick"
Command="{Binding DataContext.MiddleClickCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
<MouseBinding Gesture="Ctrl+RightClick"
Command="{Binding DataContext.CtrlRightClickCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
<MouseBinding MouseAction="RightClick"
Command="{Binding DataContext.RightClickCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"/>
</ContentPresenter.InputBindings>
</ContentPresenter>
</Border>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.MouseOver.Background}"/>
<Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.MouseOver.Border}"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Selector.IsSelectionActive" Value="False"/>
<Condition Property="IsSelected" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Background}"/>
<Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedInactive.Border}"/>
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Selector.IsSelectionActive" Value="True"/>
<Condition Property="IsSelected" Value="True"/>
</MultiTrigger.Conditions>
<Setter Property="Background" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Background}"/>
<Setter Property="BorderBrush" TargetName="Bd" Value="{StaticResource Item.SelectedActive.Border}"/>
</MultiTrigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="TextElement.Foreground" TargetName="Bd" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
I want to add a DataTrigger to my base TextBox style so that it sets the foreground color to a different value if it is inside of a DataGridCell that is selected. Here is what my trigger looks like:
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridCell}}, Path=IsSelected}"
Value="True">
<Setter Property="Foreground"
Value="White" />
</DataTrigger>
</Style.Triggers>
This works great, except that when my TextBox is not in a DataGrid the Binding fails and writes an exception to the output window. How can I prevent this.
I basically want to say if Parent is a DataGridCell then apply this trigger otherwise ignore it.
In general just only apply the style where applicable. If you want implicit application use nested styles:
<Style TargetType="{x:Type DataGrid}">
<Style.Resources>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridCell}}, Path=IsSelected}"
Value="True">
<Setter Property="Foreground" Value="White" />
</DataTrigger>
</Style.Triggers>
</Style>
</Style.Resources>
</Style>
If you have other parts which you want to apply to all TextBoxes take out those parts in a serarate style and use BasedOn in the style which applies to the TextBoxes inside the DataGrid.
Edit: MultiDataTrigger seems to return right away if a condition is not met so you can avoid binding errors:
<Style TargetType="{x:Type TextBox}">
<Style.Resources>
<vc:HasAncestorOfTypeConverter x:Key="HasAncestorOfTypeConverter" AncestorType="{x:Type DataGridCell}" />
</Style.Resources>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition
Binding="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource HasAncestorOfTypeConverter}}"
Value="True" />
<Condition
Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridCell}}, Path=IsSelected}"
Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="Foreground" Value="Red" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
public class HasAncestorOfTypeConverter : IValueConverter
{
public Type AncestorType { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null) return false;
DependencyObject current = value as DependencyObject;
while (true)
{
current = VisualTreeHelper.GetParent(current);
if (current == null)
{
return false;
}
if (current.GetType() == AncestorType)
{
return true;
}
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
This of course causes quite som overhead so it might not be such a good solution, then again if the RelativeSource-binding fails it also had to go up the tree first.
I have a style template (below) that does not update when my Tag binding updates. The data itself updates and I receive no binding errors so expect everything is bound correctly it is just that the style doesn't seem to update at all. I have notifypropertychanged events in all the right places afaik so doubt this is the issue.
Thanks
<Style x:Key="CompareTemplate" TargetType="TextBlock">
<!--Setter Property="Foreground" Value="#FF760000" /-->
<Setter Property="Foreground" Value="#FFBCBCBC" />
<Style.Triggers>
<Trigger Value="True" Property="Tag">
<Setter Property="Foreground" Value="#FF007602" />
</Trigger>
<Trigger Value="False" Property="Tag">
<Setter Property="Foreground" Value="#FF760000" />
</Trigger>
</Style.Triggers>
</Style>
And I use this template like so:
<TextBlock Style="{DynamicResource CompareTemplate}" Tag="{Binding UnitComparer.CommsID, FallbackValue=True}" Text="Comms ID:" />
Tag is of type object. I think that your Viewmodel assings a bool to it. WPF is able to convert between strings and objects but seemingly not between bool and object. One solution is to use a IValueConverter to change the bool to a string:
<Window x:Class="BindToTagSpike.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BindToTagSpike"
Title="Window1" Height="300" Width="300">
<StackPanel>
<StackPanel.Resources>
<local:ObjectToString x:Key="ObjectToString"/>
<Style x:Key="CompareTemplate" TargetType="TextBlock">
<Style.Triggers>
<Trigger Value="True" Property="Tag">
<Setter Property="Foreground" Value="Red" />
</Trigger>
<Trigger Value="False" Property="Tag">
<Setter Property="Foreground" Value="YellowGreen" />
</Trigger>
</Style.Triggers>
</Style>
</StackPanel.Resources>
<TextBlock Style="{StaticResource CompareTemplate}"
Name="TaggedTextBlock"
Tag="{Binding TagValue,
Converter={StaticResource ObjectToString}}"/>
<Button Click="Button_Click">Change Style</Button>
</StackPanel>
</Window>
using System;
using System.Windows;
using System.Windows.Data;
using System.ComponentModel;
namespace BindToTagSpike
{
public partial class Window1 : Window, INotifyPropertyChanged
{
public Window1()
{
InitializeComponent();
tagValue = false;
TaggedTextBlock.Text = "Test";
DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
TagValue=!TagValue;
}
private bool tagValue;
public bool TagValue
{
get { return tagValue; }
set
{
tagValue = value;
if (PropertyChanged != null)
PropertyChanged(this,new PropertyChangedEventArgs("TagValue"));
}
}
#region INotifyPropertyChanged Members
public event PropertyChangedEventHandler PropertyChanged;
#endregion
}
public class ObjectToString : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value.ToString();
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
#endregion
}
}
Sometime when your template / style is not being applied as expected, WPF might not think the TargetType might matches the control type. Try the code below and see if that helps at all:
<Style x:Key="CompareTemplate" >
<!--Setter Property="Control.Foreground" Value="#FF760000" /-->
<Setter Property="Control.Foreground" Value="#FFBCBCBC" />
<Style.Triggers>
<Trigger Value="True" Property="Control.Tag">
<Setter Property="Control.Foreground" Value="#FF007602" />
</Trigger>
<Trigger Value="False" Property="Control.Tag">
<Setter Property="Control.Foreground" Value="#FF760000" />
</Trigger>
</Style.Triggers>
</Style>
Cheers,
Berryl