Reputation: 638
I have the following Binding in an EventTrigger:
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="PreviewMouseDown">
<SoundPlayerAction Source="{Binding Path=SoundFile, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource soundFileConverter}}" />
</EventTrigger>
...
The process is the following: The custom control (that's its template) has a property named SoundFile, an enum type. In the converter this enum value should be converted to an Uri to pass it to the SoundPlayerAction.
Here's the problem: The converter isn't called anyway. The output window presents the following error:
Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=SoundFile; DataItem=null; target element is 'SoundPlayerAction' HashCode=46763000); target property is 'Source' (type 'Uri')
What's wrong with the binding expression?
EDIT:
For a better overview, here's the entire template of the control:
<Style TargetType="{x:Type controls:Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:Button}">
<Border Name="Border"
Background="{TemplateBinding Background}"
BorderBrush="Transparent"
BorderThickness="0">
<Border.CornerRadius>
<MultiBinding Converter="{StaticResource areaCornerRadiusConverter}">
<MultiBinding.Bindings>
<Binding Path="RoundType" RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Path="ActualHeight" RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding.Bindings>
</MultiBinding>
</Border.CornerRadius>
<TextBlock Margin="{Binding Path=RoundType,
RelativeSource={RelativeSource TemplatedParent},
Converter={StaticResource buttonMarginConverter}}"
FontSize="{TemplateBinding FontSize}"
Style="{StaticResource innerTextBlock}"
Text="{TemplateBinding Text}" />
</Border>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="PreviewMouseDown">
<SoundPlayerAction Source="{Binding Path=SoundFile, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource soundFileConverter}}" />
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
EDIT 2:
I tried it in another way: Setting the name attribute of the SoundPlayerAction to PART_SoundPlayerAction and retrieving it from the code-behind with GetTemplateChild. But GetTemplateChild returns always null. That's really annoying. Nothing seems to work...
EDIT 3:
Now with the answer of Blachshma I got it that the converter is called during the initalization of the control. But not when the property is changing. Furthermore, the value which is returned by the converter, isn't applied as Source to the SoundPlayerAction.
I implemented the BindingProxy:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public SoundFile Data
{
get { return (SoundFile)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(SoundFile), typeof(BindingProxy), new UIPropertyMetadata(SoundFile.None));
}
And I changed the Path=Data.SoundFile
to Path=Data
. Is there any mistake?
EDIT 4:
The solution with the MakeSoundCommand is working perfectly. Thanks a lot to Blachshma.
Upvotes: 2
Views: 2034
Reputation: 17395
Well you're right, trying to use binding in that specific place (An EventTrigger inside a style) is quite a pain.
I've found that the best way to solve this issue is to use both Freezables (to inherit the DataContext) together with static BindingProxies...
To solve it in your case, start by making a freezable class, we'll call it BindingProxy:
public class BindingProxy : Freezable
{
#region Overrides of Freezable
protected override Freezable CreateInstanceCore()
{
return new BindingProxy();
}
#endregion
public object Data
{
get { return (object)GetValue(DataProperty); }
set { SetValue(DataProperty, value); }
}
// Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc...
public static readonly DependencyProperty DataProperty =
DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null));
}
So our BindingProxy class implements Freezable
and exposes a property named Data
. This will be the Property which will hold the DataContext.
Now, in our resources, we'll create a StaticResource that uses this class... We'll name it "proxy":
<Window.Resources>
<local:BindingProxy x:Key="proxy" Data="{Binding}" />
Notice, that we bind the Window's DataContext into the Data
property. This will allow use to access it from the SoundPlayerAction
.
Finally, let's update the SoundPlayerAction to work with our proxy:
<EventTrigger RoutedEvent="PreviewMouseDown">
<SoundPlayerAction Source="{Binding Source={StaticResource proxy}, Path=Data.SoundFile,Converter={StaticResource soundFileConverter}}" />
</EventTrigger>
Instead of the regular binding you used, we're binding to a StaticResource, the instance of our BindingProxy class. Since the Data
property is binded to the DataContext of the window, we can get the SoundFile property using Data.SoundFile
.
And as an added bonus, since all this is done via Source={Binding
you can stil call your soundFileConverter to convert the string to a URI.
Update:
OP does not want to put the BindingProxy
class inside some <Window.Resources>
tag every time he uses this control (which is legitimate), so in this version, we're going to put all the logic inside the ResourceDictionary and modify a bit of XAML so that it continues to work..
So in our resource dictionary, we're going to use System.Windows.Interactivity
and Microsoft.Expression.Interactions
(both freely available through the Blend SDK)
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
Since in the previous version we could count on the binding being preformed through the Window.Resources, this time we're going to have to do it ourselves... We'll modify the original template to add an <i:Interaction.Triggers>
which will replace the EventTrigger
but allows sounds to play through a dedicated command:
<local:MakeSoundCommand x:Key="soundCommand"/>
<Style TargetType="{x:Type controls:Button}" >
....
....
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewMouseDown">
<local:EventToCommand Command="{StaticResource soundCommand}" CommandParameter="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SoundFile, Converter={StaticResource soundFileConverter}}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<TextBlock Margin="{Binding Path=RoundType,....
This is placed right before the TextBlock, what it does is handle the PreviewMouseDown
event and calls the a command named soundCommand
of type MakeSoundCommand
.
This is the implementation of MakeSoundCommand:
public class MakeSoundCommand : ICommand
{
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
Uri uri = parameter as Uri;
if (uri != null)
{
StreamResourceInfo sri = Application.GetResourceStream(uri);
SoundPlayer simpleSound = new SoundPlayer(sri.Stream);
simpleSound.Play();
}
}
The rest of the code remains the same.
Note: The EventToCommand
used is the one from the MVVM Light Toolkit and can be downloaded here
Final Result:
Generic.xaml:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:local="<YOUR NAMESPACE>">
<local:MakeSoundCommand x:Key="soundCommand" />
<local:SoundFileToUriConverter:Key="soundFileConverter" />
<Style TargetType="{x:Type controls:Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType=" {x:Type controls:Button}">
<Border Name="Border"
Background="{TemplateBinding Background}"
BorderBrush="Transparent"
BorderThickness="0">
<Border.CornerRadius>
....
</Border.CornerRadius>
<i:Interaction.Triggers>
<i:EventTrigger EventName="PreviewMouseDown">
<local:EventToCommand Command="{StaticResource soundCommand}" CommandParameter="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SoundFile, Converter={StaticResource soundFileConverter}}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<TextBlock Margin="{Binding Path=RoundType,
RelativeSource={RelativeSource TemplatedParent}}" .... />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Upvotes: 2