MahApps Metro SplitButton error on default copy template - wpf

Had created blank WPF application in .Net Core 3.1, added MahApps Metro package 2.0.0. Had added simple SplitButton and implemented simple ViewModel to show items in SplitButton. This works fine.
Split button XAML code:
<MahApps:SplitButton Style="{DynamicResource SplitButtonStyle}"
Name="BrowserList"
MinWidth="80"
BorderThickness="1"
BorderBrush="LightGray"
MinHeight="31"
Width="120"
Margin="5,0,0,0"
ItemsSource="{Binding BrowsersAll, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
IsEnabled="true" >
<MahApps:SplitButton.ItemTemplate>
<DataTemplate>
<Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock Margin="5,0,0,0" Text="{Binding FullName}" VerticalAlignment="Center" />
</StackPanel>
</Grid>
</DataTemplate>
</MahApps:SplitButton.ItemTemplate>
</MahApps:SplitButton>
Model for main window:
public partial class MainWindow : MetroWindow
{
public Model data;
public MainWindow()
{
InitializeComponent();
data = new Model();
data.BrowsersAll.Add(new Browser() { FullName = "one" } );
data.BrowsersAll.Add(new Browser() { FullName = "two" } );
data.BrowsersAll.Add(new Browser() { FullName = "three" } );
this.DataContext = data;
}
}
However, moment I try to edit template copy, without making any changes to template it shows errors:
at System.Windows.Markup.XamlReader.RewrapException(Exception e, IXamlLineInfo lineInfo, Uri baseUri)
at System.Windows.Markup.WpfXamlLoader.Load(XamlReader xamlReader, IXamlObjectWriterFactory writerFactory, Boolean skipJournaledProperties, Object rootObject, XamlObjectWriterSettings settings, Uri baseUri)
at System.Windows.Markup.WpfXamlLoader.LoadBaml(XamlReader xamlReader, Boolean skipJournaledProperties, Object rootObject, XamlAccessLevel accessLevel, Uri baseUri)
at System.Windows.Markup.XamlReader.LoadBaml(Stream stream, ParserContext parserContext, Object parent, Boolean closeStream)
at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator)
at WpfApp8splitb.MainWindow.InitializeComponent() in C:\Projects\test\core\WpfApp8splitb\WpfApp8splitb\MainWindow.xaml:line 1
And here is exception error:
ArgumentException: Must have non-null value for 'Setter.Property'.
Link to complete solution file (25.5Kb):
https://easyupload.io/xoh43p

Instead of relying on Visual Studio to decompile the template for you, you could just download the uncompiled version from GitHub.
I belive VS has issues with this kind of triggers with template bindings:
<Trigger SourceName="PART_Expander" Property="IsPressed" Value="True">
<Setter TargetName="PART_Expander" Property="Foreground"
Value="{Binding RelativeSource={RelativeSource TemplatedParent},
Path=ArrowPressedBrush, Mode=OneWay}" />
</Trigger>

Related

Accessing a static object from Style/template in Generic.xml?

I have a double called LoadAnimAngle which simply holds the angle of a spinning loading icon, which gets rotated over time. This variable is defined in my MainViewModel class. I'm using this same variable across all places that has a spinning loading icon.
I need it inside a custom control that is defined in Generic.xml with a style/template. Here is the part where I'm binding to LoadAnimAngle:
<v:ColoredImage Image="{StaticResource LoadingIcon}" Color="{StaticResource DarkBlueClick}" RenderTransformOrigin="0.5, 0.5" VerticalAlignment="Center" Width="32" Height="32" Margin="0,0,0,0" Visibility="{Binding IsBusy, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToVisibility}}">
<v:ColoredImage.RenderTransform>
<RotateTransform Angle="{Binding MainViewModel.LoadAnimAngle, RelativeSource={RelativeSource TemplatedParent}}}"/> //here is the error
</v:ColoredImage.RenderTransform>
</v:ColoredImage>
The custom control has a property that is binding to my instance of MainViewModel, like so:
public MainViewModel MainViewModel { get { return MainViewModel.instance; } }
Inside the constructor of MainViewModel I simply set:
instance = this;
The problem is that Generic.xml gets loaded before my MainViewModel class, causing the instance to be null for the frame before the graphics have loaded, after everything is done loaded, everything works. How could I resolve this problem?
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=MainViewModel.LoadAnimAngle; DataItem=null; target element is 'RotateTransform' (HashCode=66939890); target property is 'Angle' (type 'Double')
Notice: I do understand that the error is harmless and does not effect anything for the end user, however seeing that error every time I debug causes me emotional pain.
I need to somehow load MainViewModel before Generic, OR, tell xaml to not try to get the data from LoadAnimAngle until MainViewModel != null.
EDIT
I get the same error after I made changes so that I do not directly bind to the instance of MainViewModel. So I think my evaluation of the case of the problem is wrong.
I added
public double LoadAnimAngle
{
get
{
if (MainViewModel.instance != null)
{
return MainViewModel.instance.LoadAnimAngle;
}
return 0;
}
}
to the view model (instead of return MainViewModel.instance)
Then I changed the binding to:
Angle="{Binding Path=LoadAnimAngle, RelativeSource={RelativeSource TemplatedParent}}"
I get the same error:
System.Windows.Data Error: 2 : Cannot find governing FrameworkElement or FrameworkContentElement for target element. BindingExpression:Path=LoadAnimAngle; DataItem=null; target element is 'RotateTransform' (HashCode=21529561); target property is 'Angle' (type 'Double')
If the problem is not that the MainViewModel.instance is NULL, then what is it that causes the problem? I have problems decoding the language in the error message. What exactly is wrong and why?
EDIT 2
Relevant context (?)
<Style TargetType = "{x:Type v:ComPortButton}" >
<Setter Property = "Background" Value = "{StaticResource Milky}"/>
<Setter Property = "ColorPalette" Value = "{StaticResource MilkyPalette}"/>
<Setter Property = "Foreground" Value = "{StaticResource Black}"/>
<Setter Property = "BorderColor" Value = "{StaticResource Milky}"/>
<Setter Property="IsBasicTextButton" Value="False"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type v:ComPortButton}">
<Grid>
<Grid Visibility="{Binding Path=IsBasicTextButton, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource InverseBooleanToVisibility}}">
<Border BorderBrush="{TemplateBinding BorderColor}" Background="{TemplateBinding Background}" Width="128" Height="140" BorderThickness="1"/>
//REMOVED IREELEVANT CODE
<v:ColoredImage Image="{StaticResource LoadingIcon}" Color="{StaticResource DarkBlueClick}" RenderTransformOrigin="0.5, 0.5" VerticalAlignment="Center" Width="32" Height="32" Margin="0,0,0,0" Visibility="{Binding IsBusy, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToVisibility}}">
<v:ColoredImage.RenderTransform>
<RotateTransform Angle="{Binding MainViewModel.LoadAnimAngle, RelativeSource={RelativeSource TemplatedParent}}}"/> //here is the error
</v:ColoredImage.RenderTransform>
</v:ColoredImage>
</Grid>
//REMOVED IRRELEVANT CONTROL
</Grid>
//REMOVED IRRELEVANT CONTEXT MENU
</Grid>
//REMOVED IRRELEVANT TRIGGERS
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
EDIT 3
The source of the error seems to be completely different from I first thought. The error seems to have something to do with RenderTransform, because I can access the property without errors from other places.
Like this:
// NO ERROR FOR TEXT BLOCK
<TextBlock Text="{Binding MainViewModel.LoadAnimAngle, RelativeSource={RelativeSource TemplatedParent}}"/>
<v:ColoredImage Image="{StaticResource LoadingIcon}" Color="{StaticResource DarkBlueClick}" RenderTransformOrigin="0.5, 0.5" VerticalAlignment="Center" Width="32" Height="32" Margin="0,0,0,0" Visibility="{Binding IsBusy, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToVisibility}}">
<v:ColoredImage.RenderTransform>
// ERROR FOR ROTATETRANSFORM
<RotateTransform Angle="{Binding MainViewModel.LoadAnimAngle, RelativeSource={RelativeSource TemplatedParent}}"/>
</v:ColoredImage.RenderTransform>
</v:ColoredImage>
But I also get the error when I do not reference MainViewModel. I created a new property like this:
public double LoadAnimAngle
{
get
{
return 0;
}
}
Then I used it in the Template like this:
<v:ColoredImage Image="{StaticResource LoadingIcon}" Color="{StaticResource DarkBlueClick}" RenderTransformOrigin="0.5, 0.5" VerticalAlignment="Center" Width="32" Height="32" Margin="0,0,0,0" Visibility="{Binding IsBusy, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource BooleanToVisibility}}">
<v:ColoredImage.RenderTransform>
<RotateTransform Angle="{Binding LoadAnimAngle, RelativeSource={RelativeSource TemplatedParent}}"/>
</v:ColoredImage.RenderTransform>
</v:ColoredImage>
But i get the EXACT same error!
So, the property works, everything works. It's just that RenderTransform is like outside of the VisualTree for the first frame when it is instantiated? Or something like that, i guess? Something different is happening in RenderTransform that makes it so it doesnt like my binding.
And i probably wasnt clear about the structure.
ComPortButton is a Custom Control (.cs file with Template/Style in Generic.xml).
ComPortButton uses ComPortVM as it's DataContext.
I want to access the spinning value globally, different controls, different windows, different everything, globally.
I have a MainViewModel in which i currently store the value, since it gives global access, since it
EDIT 4
Solved it and posted the solution below
After i figured it out that it was RenderTransform that was the problem and not anything else it was easy to find solutions online, seems that many people have had the same problem.
Here is the Thread that helped me solve it
The problem had something to do with VisualTree, that RenderTransform in the Template isnt hooked up to the VisualTree before the entire Control is loaded. Or something like that.
When binding like this to RotateTransform:
<v:ColoredImage.RenderTransform>
<RotateTransform Angle="{Binding LoadAnimAngle, RelativeSource={RelativeSource TemplatedParent}}"/>
</v:ColoredImage.RenderTransform>
The problem occurs. But for some reason that i did not understand, you can get rif of the error by binding to RenderTransform instead. But for that you need a Converter.
[ValueConversion(typeof(double), typeof(RotateTransform))]
public class AngleToTransform : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
return new RotateTransform((double)value);
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
Then use the converter like so:
<v:ColoredImage RenderTransform="{Binding MainViewModel.LoadAnimAngle, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource AngleToTransform}}"/>
Your control must be independent from the concrete view model type. Instead, bind internals of the control to dependency properties on this control. Then let the external view model bind to this properties (or set them locally).
This way you remove the tight coupling between the control and the DataContext, which drastically simplifies the implementation of the control. It also allows the control to be reused with any DataContext (view model).
ComPortButton.cs
class ComPortButton : Control
{
public double Angle
{
get => (double)GetValue(AngleProperty);
set => SetValue(AnglePropertyKey, value);
}
protected static readonly DependencyProperty AnglePropertyKey = DependencyProperty.RegisterReadOnly(
"Angle",
typeof(double),
typeof(ComPortButton),
new PropertyMetadata(default));
public static readonly DependencyProperty AngleProperty = AnglePropertyKey..DependencyProperty;
public double ProgressPercentage
{
get => (double)GetValue(ProgressPercentageProperty);
set => SetValue(ProgressPercentageProperty, value);
}
public static readonly DependencyProperty ProgressPercentageProperty = DependencyProperty.Register(
"ProgressPercentage",
typeof(double),
typeof(ComPortButton),
new PropertyMetadata(default(double), OnProgressPercentageChanged));
private static void OnProgressPercentageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
double percentage = (double)e.NewValue / 100;
// Enforce an angle between 0°-360°
this.Angle = Math.Max(0, Math.Min(360, 360 * percentage));
}
}
Generic.xaml
<Style TargetType="ComPortButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type v:ComPortButton}">
<v:ColoredImage>
<v:ColoredImage.RenderTransform>
<RotateTransform Angle="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Angle}" />
</v:ColoredImage.RenderTransform>
</v:ColoredImage>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Usage example
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<ComPortButton ProgressPercentage="{Binding ProgressPercentageValue}" />
</Window>

Bind TextBox.Text in Watermark attached property to localize it

I need to add Watermarks to my WPF application and I am using this watermark service to achieve it.
It works just as expected, but I need to localize the text showed as watermark and I can't find a way to achieve it.
My xaml declaration:
<TextBox
Text="{Binding Name, Mode=TwoWay}">
<Watermark:WatermarkService.Watermark>
<TextBlock
Text="{Binding <What-goes-here?>}"
/>
</Watermark:WatermarkService.Watermark>
</TextBox>
My ViewModel:
public class PersonViewModel
{
public string Name {get;set;}
public string NameWatermark {get;set;}
}
Thw following lines do NOT work:
Text="{Binding NameWatermark}"
<Window x:Name="ThisWindow"
...
Text="{Binding Path=NameWatermark,
ElementName=ThisWindow}"
Is there any way to achieve it?
Any help would be appreciated, thanks.
I realize this question is ancient, but for google time-travelers, i hit the exact same problem and solved it by using a MultiDataTrigger to hide/show the watermark element. It works because it's not embedding the element within an adorner like the original Watermark:WatermarkService.Watermark does.
<Grid>
<TextBox x:Name="messageBox" Text="{Binding Message, UpdateSourceTrigger=PropertyChanged}" Padding="5">
<TextBox.InputBindings>
<KeyBinding Key="Enter" Command="{Binding SendMessageCommand}"/>
</TextBox.InputBindings>
</TextBox>
<TextBlock Text="{text:Translate Key=WriteMessageWatermark}" IsHitTestVisible="False" Foreground="LightGray" Margin="7,6,0,0">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ElementName=messageBox, Path=Text.IsEmpty}" Value="True" />
<Condition Binding="{Binding ElementName=messageBox, Path=IsFocused}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
I think the issue is that the WatermarkService is just a static class and no dependencyobject and therefore doesn't inherit the DataContext properly to its content.
At first: Do you really need to bind to the ViewModel? I assume you want to localize more parts of your UI so moving this stuff to a dedicated service would be an option.
Something like this:
<Watermark:WatermarkService.Watermark>
<TextBlock Text="{Binding Source={x:Static Watermark:LocalizationService.Instance}, Path=[123],FallbackValue='No LocalizationService found'}"/>
</Watermark:WatermarkService.Watermark>
Singleton class for Localization:
private static volatile LocalizationService _instance;
private static object syncRoot = new Object();
public static LocalizationService Instance
{
get
{
if (_instance == null)
{
lock (syncRoot)
{
if (_instance == null)
_instance = new LocalizationService();
}
}
return _instance;
}
}
public string this[int id]
{
get
{
// do localization stuff here ...
return "Localized Value " + id;
}
}
}
If you really want to bind to the window's DataContext you can use the following workaround by assigning the ViewModel to an ObjectDataprovider and access it in your Binding:
<Grid Loaded="Grid_Loaded">
<Grid.Resources>
<ObjectDataProvider x:Key="PersonViewModelProvider" ObjectInstance="{x:Null}" IsAsynchronous="True"/>
</Grid.Resources>
<TextBox Text="{Binding Name, Mode=TwoWay}">
<Watermark:WatermarkService.Watermark>
<TextBlock Text="{Binding Source={StaticResource PersonViewModelProvider}, Path=NameWatermark}"/>
</Watermark:WatermarkService.Watermark>
</TextBox>
</Grid>
Code Behind:
private void Grid_Loaded(object sender, System.Windows.RoutedEventArgs e)
{
var panel = sender as Panel;
if (panel != null)
{
var objDataProvider = panel.Resources["PersonViewModelProvider"] as ObjectDataProvider;
objDataProvider.ObjectInstance = panel.DataContext;
}
}

ToolBar item DataTemplate binding RelativeSource search failing to find parent ToolBar

I have a ToolBar containing Buttons, some of the Buttons have only an Image for content, others have only Text. I am trying to bind the width property of the Button Image to a custom Property on my derived ToolBar class. It works sometimes but fails other times with the following error:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='NuiWpfCore.Controls.ToolBar', AncestorLevel='1''. BindingExpression:Path=IconSize; DataItem=null; target element is 'Image' (Name=''); target property is 'Width' (type 'Double')
Here is the xaml containing the element binding that is failing. The DataTemplate is returned from a DataTemplateSelector which is created inline:
<pres:ToolBar x:Class="NuiWpfCore.Controls.ToolBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:pres="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:core="clr-namespace:NuiWpfCore"
xmlns:ctrls="clr-namespace:NuiWpfCore.Controls"
xmlns:select="clr-namespace:NuiWpfCore.Selectors"
xmlns:converters="clr-namespace:NuiWpfCore.Converters"
xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase">
<ToolBar.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NuiWpfCore;component/Controls/MenuBarTemplate.xaml" />
</ResourceDictionary.MergedDictionaries>
<converters:ListPairToStringConverter x:Key="ListPairToStringConverter" />
<converters:IconMetaDataToImageConverter x:Key="IconMetaDataToImageConverter" />
<converters:IconMetaDataToImageConverterParameter x:Key="IconToImageConverterParameter"
ConvertToImage="False" Width="16" Height="16" />
</ResourceDictionary>
</ToolBar.Resources>
<ToolBar.ItemTemplateSelector>
<select:ToolBarItemDataTemplateSelector>
<!-- other DataTemplates omitted for brevity -->
<select:ToolBarItemDataTemplateSelector.IconCommand>
<DataTemplate DataType="{x:Type core:PropertyElement}">
<Button IsEnabled="{Binding Path=CanEdit}" Command="{Binding}">
<Button.Content>
<Image
Width="{Binding Path=IconSize,
RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ctrls:ToolBar}} }"
Height="{Binding Path=Width,
RelativeSource={RelativeSource Self}}"
Source="{Binding Path=MetaData,
Converter={StaticResource IconMetaDataToImageConverter},
ConverterParameter={StaticResource IconToImageConverterParameter}}"/>
</Button.Content>
</Button>
</DataTemplate>
</select:ToolBarItemDataTemplateSelector.IconCommand>
<!-- other DataTemplates omitted for brevity -->
</select:ToolBarItemDataTemplateSelector>
</ToolBar.ItemTemplateSelector>
</pres:ToolBar>
Here is the ToolBar class with the Source Property for the binding.
public partial class ToolBar : System.Windows.Controls.ToolBar, Views.IView
{
public ToolBar() : base()
{
InitializeComponent();
IconSize = 32;
}
public int IconSize { get; set; }
}
This ToolBar class is sometimes used in a ToolBarTray and other times it is not, but the bind search fails in both cases in certain scenarios.
Does anybody have any ideas as to why this might be failing?
Have you considered making IconSize on your ToolBar an inherited property?
public static readonly DependencyProperty IconSizeProperty =
DependencyProperty.RegisterAttached( "IconSize", typeof(double), typeof(ToolBar ),
new FrameworkPropertyMetadata(32, FrameworkPropertyMetadataOptions.Inherits));
public static double GetIconSize(DependencyObject target)
{
return (double)target.GetValue(IconSizeProperty);
}
public static void SetIconSize(DependencyObject target, double value)
{
target.SetValue(IconSizeProperty, value);
}
Then you can just access the IconSize like
<Button.Content>
<Image
Width="{Binding RelativeSource={RelativeSource Self}, ctrls::ToolBar.IconSize}"
Height="{Binding Path=Width,RelativeSource={RelativeSource Self}}"
Source="{Binding Path=MetaData,
Converter={StaticResource IconMetaDataToImageConverter},
ConverterParameter={StaticResource IconToImageConverterParameter}}"/>
First you should set it on your toolbar, and every other element down the tree can access this property.
Sorry out of my head, not 100% guarenteed to be correct. But the overall idea of
Value Inheritance is a good way to solve this.
The DataTemplate looks like it is being defined inside a DataTemplateSelector declaration, which isn't part of the Visual Tree, and so won't be able to navigate up from there if the Binding were being evaluated in that spot. Where is the template actually being applied?

Can I use a different Template for the selected item in a WPF ComboBox than for the items in the dropdown part?

I have a WPF Combobox which is filled with, say, Customer objects. I have a DataTemplate:
<DataTemplate DataType="{x:Type MyAssembly:Customer}">
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Address}" />
</StackPanel>
</DataTemplate>
This way, when I open my ComboBox, I can see the different Customers with their Name and, below that, the Address.
But when I select a Customer, I only want to display the Name in the ComboBox. Something like:
<DataTemplate DataType="{x:Type MyAssembly:Customer}">
<StackPanel>
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
Can I select another Template for the selected item in a ComboBox?
Solution
With help from the answers, I solved it like this:
<UserControl.Resources>
<ControlTemplate x:Key="SimpleTemplate">
<StackPanel>
<TextBlock Text="{Binding Name}" />
</StackPanel>
</ControlTemplate>
<ControlTemplate x:Key="ExtendedTemplate">
<StackPanel>
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Address}" />
</StackPanel>
</ControlTemplate>
<DataTemplate x:Key="CustomerTemplate">
<Control x:Name="theControl" Focusable="False" Template="{StaticResource ExtendedTemplate}" />
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}">
<Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</UserControl.Resources>
Then, my ComboBox:
<ComboBox ItemsSource="{Binding Customers}"
SelectedItem="{Binding SelectedCustomer}"
ItemTemplate="{StaticResource CustomerTemplate}" />
The important part to get it to work was Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ComboBoxItem}}, Path=IsSelected}" Value="{x:Null}" (the part where value should be x:Null, not True).
The issue with using the DataTrigger/Binding solution mentioned above are two-fold. The first is you actually end up with a binding warning that you can't find the relative source for the selected item. The bigger issue however is that you've cluttered up your data templates and made them specific to a ComboBox.
The solution I present better follows WPF designs in that it uses a DataTemplateSelector on which you can specify separate templates using its SelectedItemTemplate and DropDownItemsTemplate properties as well as ‘selector’ variants for both.
Note: Updated for C#9 with nullability enabled and using pattern matching during the search
public class ComboBoxTemplateSelector : DataTemplateSelector {
public DataTemplate? SelectedItemTemplate { get; set; }
public DataTemplateSelector? SelectedItemTemplateSelector { get; set; }
public DataTemplate? DropdownItemsTemplate { get; set; }
public DataTemplateSelector? DropdownItemsTemplateSelector { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
var itemToCheck = container;
// Search up the visual tree, stopping at either a ComboBox or
// a ComboBoxItem (or null). This will determine which template to use
while(itemToCheck is not null
and not ComboBox
and not ComboBoxItem)
itemToCheck = VisualTreeHelper.GetParent(itemToCheck);
// If you stopped at a ComboBoxItem, you're in the dropdown
var inDropDown = itemToCheck is ComboBoxItem;
return inDropDown
? DropdownItemsTemplate ?? DropdownItemsTemplateSelector?.SelectTemplate(item, container)
: SelectedItemTemplate ?? SelectedItemTemplateSelector?.SelectTemplate(item, container);
}
}
To make it easier to use in XAML, I've also included a markup extension that simply creates and returns the above class in its ProvideValue function.
public class ComboBoxTemplateSelectorExtension : MarkupExtension {
public DataTemplate? SelectedItemTemplate { get; set; }
public DataTemplateSelector? SelectedItemTemplateSelector { get; set; }
public DataTemplate? DropdownItemsTemplate { get; set; }
public DataTemplateSelector? DropdownItemsTemplateSelector { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider)
=> new ComboBoxTemplateSelector(){
SelectedItemTemplate = SelectedItemTemplate,
SelectedItemTemplateSelector = SelectedItemTemplateSelector,
DropdownItemsTemplate = DropdownItemsTemplate,
DropdownItemsTemplateSelector = DropdownItemsTemplateSelector
};
}
And here's how you use it. Nice, clean and clear and your templates stay 'pure'
Note: 'is:' here is my xmlns mapping for where I put the class in code. Make sure to import your own namespace and change 'is:' as appropriate.
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource MySelectedItemTemplate},
DropdownItemsTemplate={StaticResource MyDropDownItemTemplate}}" />
You can also use DataTemplateSelectors if you prefer...
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplateSelector={StaticResource MySelectedItemTemplateSelector},
DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />
Or mix and match! Here I'm using a template for the selected item, but a template selector for the DropDown items.
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource MySelectedItemTemplate},
DropdownItemsTemplateSelector={StaticResource MyDropDownItemTemplateSelector}}" />
Additionally, if you don't specify a Template or a TemplateSelector for the selected or dropdown items, it simply falls back to the regular resolving of data templates based on data types, again, as you would expect. So, for instance, in the below case, the selected item has its template explicitly set, but the dropdown will inherit whichever data template applies for the DataType of the object in the data context.
<ComboBox x:Name="MyComboBox"
ItemsSource="{Binding Items}"
ItemTemplateSelector="{is:ComboBoxTemplateSelector
SelectedItemTemplate={StaticResource MyTemplate} />
Enjoy!
Simple solution:
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Address}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</DataTemplate>
(Note that the element that is selected and displayed in the box and not the list is not inside a ComboBoxItem hence the trigger on Null)
If you want to switch out the whole template you can do that as well by using the trigger to e.g. apply a different ContentTemplate to a ContentControl. This also allows you to retain a default DataType-based template selection if you just change the template for this selective case, e.g.:
<ComboBox.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=ComboBoxItem}}"
Value="{x:Null}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<!-- ... -->
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</ComboBox.ItemTemplate>
Note that this method will cause binding errors as the relative source is not found for the selected item. For an alternate approach see MarqueIV's answer.
In addition to what said by H.B. answer, the Binding Error can be avoided with a Converter. The following example is based from the Solution edited by the OP himself.
The idea is very simple: bind to something that alway exists (Control) and do the relevant check inside the converter.
The relevant part of the modified XAML is the following. Please note that Path=IsSelected was never really needed and ComboBoxItem is replaced with Control to avoid the binding errors.
<DataTrigger Binding="{Binding
RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Control}},
Converter={StaticResource ComboBoxItemIsSelectedConverter}}"
Value="{x:Null}">
<Setter TargetName="theControl" Property="Template" Value="{StaticResource SimpleTemplate}" />
</DataTrigger>
The C# Converter code is the following:
public class ComboBoxItemIsSelectedConverter : IValueConverter
{
private static object _notNull = new object();
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// value is ComboBox when the item is the one in the closed combo
if (value is ComboBox) return null;
// all the other items inside the dropdown will go here
return _notNull;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I was going to suggest using the combination of an ItemTemplate for the combo items, with the Text parameter as the title selection, but I see that ComboBox doesn't respect the Text parameter.
I dealt with something similar by overriding the ComboBox ControlTemplate. Here's the MSDN website with a sample for .NET 4.0.
In my solution, I change the ContentPresenter in the ComboBox template to have it bind to Text, with its ContentTemplate bound to a simple DataTemplate that contains a TextBlock like so:
<DataTemplate x:Uid="DataTemplate_1" x:Key="ComboSelectionBoxTemplate">
<TextBlock x:Uid="TextBlock_1" Text="{Binding}" />
</DataTemplate>
with this in the ControlTemplate:
<ContentPresenter Name="ContentSite" IsHitTestVisible="False" Content="{TemplateBinding Text}" ContentTemplate="{StaticResource ComboSelectionBoxTemplate}" Margin="3,3,23,3" VerticalAlignment="Center" HorizontalAlignment="Left"/>
With this binding link, I am able to control the Combo selection display directly via the Text parameter on the control (which I bind to an appropriate value on my ViewModel).
I used next approach
<UserControl.Resources>
<DataTemplate x:Key="SelectedItemTemplate" DataType="{x:Type statusBar:OffsetItem}">
<TextBlock Text="{Binding Path=ShortName}" />
</DataTemplate>
</UserControl.Resources>
<StackPanel Orientation="Horizontal">
<ComboBox DisplayMemberPath="FullName"
ItemsSource="{Binding Path=Offsets}"
behaviors:SelectedItemTemplateBehavior.SelectedItemDataTemplate="{StaticResource SelectedItemTemplate}"
SelectedItem="{Binding Path=Selected}" />
<TextBlock Text="User Time" />
<TextBlock Text="" />
</StackPanel>
And the behavior
public static class SelectedItemTemplateBehavior
{
public static readonly DependencyProperty SelectedItemDataTemplateProperty =
DependencyProperty.RegisterAttached("SelectedItemDataTemplate", typeof(DataTemplate), typeof(SelectedItemTemplateBehavior), new PropertyMetadata(default(DataTemplate), PropertyChangedCallback));
public static void SetSelectedItemDataTemplate(this UIElement element, DataTemplate value)
{
element.SetValue(SelectedItemDataTemplateProperty, value);
}
public static DataTemplate GetSelectedItemDataTemplate(this ComboBox element)
{
return (DataTemplate)element.GetValue(SelectedItemDataTemplateProperty);
}
private static void PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var uiElement = d as ComboBox;
if (e.Property == SelectedItemDataTemplateProperty && uiElement != null)
{
uiElement.Loaded -= UiElementLoaded;
UpdateSelectionTemplate(uiElement);
uiElement.Loaded += UiElementLoaded;
}
}
static void UiElementLoaded(object sender, RoutedEventArgs e)
{
UpdateSelectionTemplate((ComboBox)sender);
}
private static void UpdateSelectionTemplate(ComboBox uiElement)
{
var contentPresenter = GetChildOfType<ContentPresenter>(uiElement);
if (contentPresenter == null)
return;
var template = uiElement.GetSelectedItemDataTemplate();
contentPresenter.ContentTemplate = template;
}
public static T GetChildOfType<T>(DependencyObject depObj)
where T : DependencyObject
{
if (depObj == null) return null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = (child as T) ?? GetChildOfType<T>(child);
if (result != null) return result;
}
return null;
}
}
worked like a charm. Don't like pretty much Loaded event here but you can fix it if you want
Yes. You use a Template Selector to determine which template to bind at run-time. Thus if IsSelected = False then Use this template, if IsSelected = True, use this other template.
Of Note:
Once you implement your template selector, you will need to give the templates keynames.
I propose this solution without DataTemplateSelector, Trigger, binding nor behavior.
The first step is to put the ItemTemplate (of the selected element) in the ComboBox resources and the ItemTemplate (of the item in the drop down menu) in the ComboBox.ItemsPanel resources and give both resources the same key.
The second step is to postpone the ItemTemplate resolution at run time by using both a ContentPresenter and a DynamicResource in the actual ComboBox.ItemTemplate implementation.
<ComboBox ItemsSource="{Binding Items, Mode=OneWay}">
<ComboBox.Resources>
<!-- Define ItemTemplate resource -->
<DataTemplate x:Key="ItemTemplate" DataType="viewModel:ItemType">
<TextBlock Text="{Binding FieldOne, Mode=OneWay}" />
</DataTemplate>
</ComboBox.Resources>
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Grid.IsSharedSizeScope="True"
IsItemsHost="True">
<StackPanel.Resources>
<!-- Redefine ItemTemplate resource -->
<DataTemplate x:Key="ItemTemplate" DataType="viewModel:ItemType">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="GroupOne" />
<ColumnDefinition Width="10" SharedSizeGroup="GroupSpace" />
<ColumnDefinition Width="Auto" SharedSizeGroup="GroupTwo" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding FieldOne, Mode=OneWay}" />
<TextBlock Grid.Column="2" Text="{Binding FieldTwo, Mode=OneWay}" />
</Grid>
</DataTemplate>
</StackPanel.Resources>
</StackPanel>
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<ComboBox.ItemTemplate>
<DataTemplate>
<ContentPresenter ContentTemplate="{DynamicResource ItemTemplate}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>

WPF CommandParameter not updated when set in Context Menu

I have a wpf window with several text box controls. I need to apply a common style that would apply a context menu to each control and i have defined it globally as follows,
<ContextMenu x:Key="textBoxMenu">
<Separator/>
<MenuItem Header="Affirm"
Command="{Binding Path=AffirmCommand}"
CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type TextBox},AncestorLevel=1}}"/>
</ContextMenu>
<Style TargetType="{x:Type TextBox}" x:Key="TextBoxAffirmMenuStyle">
<Setter Property="ContextMenu" Value="{DynamicResource textBoxMenu}" />
</Style>
I Have used a Command to execute the appropriate method depending on the target of the context menu, which is in this case the text box.
To identify the controls uniquely, i have set the "Tag" property of each control with a unique string and i access this tag from the command parameter which is set to the target text box Control itself.
private bool CanAffirmExecute(object param)
{
string columnName = (param as FrameworkElement).Tag as string;
if (this.CheckIsAffirmed(columnName))
return true;
else
return false;
}
private void AffirmExecute(object param)
{
string columnName = (param as FrameworkElement).Tag as string;
this.Affirm(columnName);
}
The problem with this is that once the command parameter gets set to a particular control,
it will not change on subsequent context menu operations when right clicked on a different control. the Command parameter remains static and gets only the tag value set in the first control.
How can i get this to work so that i can access each of the tag values of the controls using the command?
thanks.
ContextMenu is at the root of its own visual tree, so any binding using RelativeSource.FindAncestor does not go past the ContextMenu.
A work around is to use a two stage binding with the PlacementTarget property as follows,
and to analyse the object parameter in the method OnAffirmCommand(object obj) to control your behaviour. In this case the object is the actual TextBox.
Here is the context menu definition:
<Window.Resources>
<ContextMenu x:Key="textBoxMenu">
<Separator/>
<MenuItem Header="Affirm"
Command="{Binding Path=AffirmCommand}"
CommandParameter="{Binding PlacementTarget.Tag,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type ContextMenu}}}"/>
</ContextMenu>
<Style TargetType="{x:Type TextBox}" x:Key="TextBoxAffirmMenuStyle">
<Setter Property="ContextMenu" Value="{StaticResource textBoxMenu}" />
</Style>
</Window.Resources>
Here are the text boxes:
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBox Grid.Row="0" ContextMenu="{StaticResource textBoxMenu}" Tag="{Binding RelativeSource={RelativeSource Self}}" Text="text in box 1"/>
<TextBox Grid.Row="1" ContextMenu="{StaticResource textBoxMenu}" Tag="{Binding RelativeSource={RelativeSource Self}}" Text="text in box 2"/>
<TextBox Grid.Row="2" ContextMenu="{StaticResource textBoxMenu}" Tag="{Binding RelativeSource={RelativeSource Self}}" Text="text in box 3"/>
</Grid>
Here is the command code from a ViewModel:
public class MainViewModel : ViewModelBase
{
public ICommand AffirmCommand { get; set; }
public MainViewModel()
{
AffirmCommand = new DelegateCommand<object>(OnAffirmCommand, CanAffirmCommand);
}
private void OnAffirmCommand(object obj)
{
}
private bool CanAffirmCommand(object obj)
{
return true;
}
}

Resources