WPF: Multiple content presenters in a custom control? - c#

I'm trying to have a custom control that requires 2 or more areas of the XAML to be defined by a child control - that inherits from this control. I'm wondering if there's a way to define multiple contentpresenters and one which acts as the default content presenter
<MyControl>
<MyControl.MyContentPresenter2>
<Button Content="I am inside the second content presenter!"/>
</MyControl.MyContentPresenter2>
<Button Content="I am inside default content presenter" />
</MyControl>
Is this possible, how do I define this in the custom control's template?

The template can just bind the separate ContentPresenter instances like this (I've only set one property here but you'll likely want to set others):
<ContentPresenter Content="{TemplateBinding Content1}"/>
<ContentPresenter Content="{TemplateBinding Content2}"/>
The control itself should expose two properties for content and set the default using the ContentPropertyAttribute:
[ContentProperty("Content1")]
public class MyControl : Control
{
// dependency properties for Content1 and Content2
// you might also want Content1Template, Content2Template, Content1TemplateSelector, Content2TemplateSelector
}

You can use an "ItemsControl" with a custom template.
<ItemsControl>
<ItemsControl.Style>
<Style TargetType="ItemsControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<StackPanel Orientation="Horizontal">
<ContentControl Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Items[0]}"/>
<ContentControl Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Items[1]}"/>
<ContentControl Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Items[2]}"/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ItemsControl.Style>
<TextBlock Text="Item 1"/>
<TextBlock Text="Item 2"/>
<TextBlock Text="Item 3"/>
</ItemsControl>

Here's another option that doesn't require making a custom control and is more typesafe than doing the ItemsControl thing (if type safety is something you want..perhaps not):
...Use an attached property!
Create an attached property of the appropriate type. We happened to need a text control so I did a string TextContent attached property. Then create a TemplateBinding to it from the template, and when instantiating in Xaml set it there as well. Works nicely.

Related

UWP Inherit from Control

For example I have a UserControl like this:
<UserControl x:Class="SMPlayer.ScrollingTextBlock">
<ScrollViewer
x:Name="TextScrollViewer"
HorizontalScrollBarVisibility="Hidden"
PointerEntered="TextScrollViewer_PointerEntered"
VerticalScrollBarVisibility="Disabled">
<StackPanel>
<TextBlock x:Name="NormalTextBlock" />
<TextBlock x:Name="PointerOverTextBlock" Visibility="Collapsed" />
</StackPanel>
</ScrollViewer>
</UserControl>
I want this UserControl still to be treated as a normal TextBlock. For example <ScrollingTextBlock Text="Something"/>. It is just a TextBlock with more functionalities, or in other words, another control that inherits from TextBlock. Because there are a lot of properties, I don't want to do this manually by adding DependencyProperty and do things like public string Text { get; set; }. It is just too much work.
How can I achieve that? I think this question might have been asked but I am not sure how to properly paraphrase it.
If you want to implement <ScrollingTextBlock Text="Something"/> in UserControl, you still need to add DependencyProperty to achieve it.
If you want your control to "be treated as a normal TextBlock", then you don't have any other choice than inheriting from TextBlock. This is what inheritance is for.
Otherwise you indeed have to add properties to your UserControl and bind them by yourself, even though this is a lot of work this is due to the poor flexibility of the UserControl. You cannot have a Text property on an object unless it inherits from a TextBlock or you add it yourself.
Alternatively you can use templating to re-template a ContentControl like this:
public class ScrollingContent : ContentControl { }
<Window.Resources>
<Style TargetType="local:ScrollingContent">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ScrollingContent">
<ScrollViewer
x:Name="TextScrollViewer"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Disabled">
<StackPanel>
<TextBlock x:Name="NormalTextBlock" />
<ContentPresenter></ContentPresenter>
</StackPanel>
</ScrollViewer>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Grid>
<local:ScrollingContent>
<TextBlock Text="Whatever control I want" Foreground="Red"></TextBlock>
</local:ScrollingContent>
</Grid>
But then again, your control is not really a TextBlock.

Multi-stage binding from ViewModel to Custom-Control to controls on ControlTemplate not working

I have a MVVM project with a View and a ViewModel in its DataContext.
In this project I have a class ComboBoxCustom which inherits from ComboBox. I define some additional functionality in my ComboBoxCustom class.
To this ComboBoxCustom class I assign a control template to define its appearance.
The (simplified) style defining the (simplified) control template looks like:
<Style TargetType="{x:Type lib:ComboBoxCustom}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type lib:ComboBoxCustom}">
<StackPanel>
<TextBlock Text="{TemplateBinding TextPropertyInComboBoxCustom}"/>
<ComboBox DataContext="{TemplateBinding DataContext}"
ItemsSource="{TemplateBinding ItemsSource}"
DisplayMemberPath="{TemplateBinding DisplayMemberPath}"
SelectedValuePath="{TemplateBinding SelectedValuePath}"
SelectedValue="{TemplateBinding SelectedValue}"
SelectedItem="{TemplateBinding SelectedItem}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Which resides in a ResourceDictionary. The real control template has some additional features which are left out since they are not relevant for the question.
I use this ComboBoxCustom control in my View using:
<lib:ComboBoxCustom ItemsSource="{Binding MyObservableCollectionOfMyObjects}"
TextPropertyInComboBoxCustom="MyText"
DisplayMemberPath="MyDescription"
SelectedValuePath="MyValue"
SelectedItem="{Binding SelectedMyObject, Mode=TwoWay}"/>
The view is ok, and all items get loaded in the ComboBox which I can select.
The problem is that when I select a different item in the ComboBox, the property SelectedMyObject in my ViewModel does not get updated and consequently its setter is not called. Therefore, the (correct) information about the selected object is not available in my ViewModel.
When I use <ComboBox .../> (without the TextPropertyInComboBoxCustom property) instead of <lib:ComboBoxCustom .../> everything works just fine but then I don't have the additional functionality defined in ComboBoxMessage which I need.
Can anyone tell me what is wrong and how to fix this issue so I can use ComboBoxMessage in my view? Preferably without breaking the MVVM pattern.
Thank you!
Thanx to ASh's comment and information in this post.
The problem is that the TemplateBinding is one way. Therefore, all information from the ViewModel can get into the controls in the template. But not the other way around.
The solution is to specify a normal binding as:
SelectedItem ="{Binding SelectedItem, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
Whichs does about the same as a TemplateBinding but is two way.
The control template has become:
<Style TargetType="{x:Type lib:ComboBoxCustom}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type lib:ComboBoxCustom}">
<StackPanel>
<TextBlock Text="{TemplateBinding TextPropertyInComboBoxCustom}"/>
<ComboBox DataContext="{TemplateBinding DataContext}"
ItemsSource="{TemplateBinding ItemsSource}"
DisplayMemberPath="{TemplateBinding DisplayMemberPath}"
SelectedValuePath="{TemplateBinding SelectedValuePath}"
SelectedValue="{TemplateBinding SelectedValue}"
SelectedItem ="{Binding SelectedItem, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
I am not sure about the SelectedValue property though. With the template like this, it works both when I use the SelectedValue property or the SelectedItem property.
The Mode=TwoWay option can be omitted in the view since the default binding mode for SelectedItem is already two way. The view line becomes:
<lib:ComboBoxCustom ItemsSource="{Binding MyObservableCollectionOfMyObjects}"
TextPropertyInComboBoxCustom="MyText"
DisplayMemberPath="MyDescription"
SelectedValuePath="MyValue"
SelectedItem="{Binding SelectedMyObject}"/>
Bind SelectedValue to the property in ViewModel
SelectedValue="{Binding SelectedMyObject, Mode=TwoWay}"
in your lib:ComboBoxCustom

AutomationElement for WPF ToggleButton - have ClassName Button

I am using ToggleButtons in the ToolBar and I want to get and use them in the UI Automation tests, but when I check AutomationElement.Current for these buttons, its ClassName property is Button, while I would expect ToggleButton
The xaml is not starightforward, so I mention it here :
<ToolBar ItemsSource="{Binding}"/>
for the type that's in the ItemsSource I have a DataTemplate:
<DataTemplate DataType="{x:Type myViewModelType}">
<ContentPresenter Content="{Binding}" ContentTemplate="{StaticResource MyToolBarElementTemplate}">
<ContentPresenter.Resources>
<Style TargetType="{x:Type ToggleButton}" BasedOn="{StaticResource ThisStyleSetsWidthAndHeight}"/>
</ContentPresenter.Resources>
</ContentPresenter>
</DataTemplate>
the style is defined as follows:
<Style TargetType="{x:Type ButtonBase}" x:Key="ThisStyleSetsWidthAndHeight">
<Setter Property="styles:AttachedProperties.ContentWidth" Value="32"/>
<Setter Property="styles:AttachedProperties.ContentHeight" Value="32"/>
</Style>
and the content template looks like this:
<DataTemplate x:Key="MyToolBarElementTemplate" DataType="{x:Type myViewModelType}">
<ToggleButton x:Name="AutomationIdThatIGetOk">
...
</ToggleButton>
</DataTemplate>
I am a bit new to Automation Framework, I guess it has to do with all these templates and styles, but is there any way to get the proper AutomationPeer instance created for this ToggleButton?
...but when I check AutomationElement.Current for these buttons, its ClassName property is Button, while I would expect ToggleButton
Your expectation is wrong because the ToggleButtonAutomationPeer class actually returns the string "Button" from its GetClassNameCore() method: https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Automation/Peers/ToggleButtonAutomationPeer.cs,a58abe77888c16cd
So you are getting the proper instance.

WPF Content View not displaying DataTemplate

I have a ContentControl which will not display any XAML from its DataTemplate, and I feel certain that the problem I'm facing will be obvious for those with better WPF codemancy than myself. I have substituted "Object" for my object name where appropriate for confidentiality reasons.
In my MainWindow.xaml I have this:
<ContentControl x:Name="ObjectDetailView"
Margin="20,20,20,20" Grid.Row="2" Grid.Column="1"
DataContext="{Binding SelectedItem, ElementName=ObjectListView}"
Template="{DynamicResource DetailControlTemplate}"
ContentTemplate="{DynamicResource DetailDataTemplate}"/>
I keep my templates in separate files to keep code readable. The template is in a DataResources.xaml file that is being successfully used for the ListView. The code for the content/template in question is:
<ControlTemplate x:Key="DetailControlTemplate">
<Border Style="{StaticResource ObjectDetailBorderStyle}">
<ContentPresenter/>
</Border>
</ControlTemplate>
<DataTemplate x:Key="DetailDataTemplate" DataType="{x:Type model:Object}">
<!-- Valid XAML -->
</DataTemplate>
In my Designer (both in VS and Blend) The border/background gradient displays, but nothing further. Same for the running program.
If I move the <!-- Valid XAML --> into the Control Template, it displays fine, but I don't believe that's kosher, and I also don't believe that the data-binding will work that way. Please correct me if I'm wrong.
ObjectListView is a ListView populated dynamically from my VM, and I'm using MVVM. That all works just fine. I'd prefer this ContentControl only appears once there is a valid selected object in the list view, but that's UX sugar at this point, thus my only concern is to get this content control displaying my model's data.
I'm also fairly new to StackOverflow, so if I missed anything or made an error in posting this question, please let me know. I've not had luck with searching for this issue over the last few hours, as I don't want to waste your time.
Two things. You did not set the actual Content of the ContentControl, but only its DataContext. You should instead write this:
<ContentControl ...
Content="{Binding SelectedItem, ElementName=ObjectListView}"
Template="{DynamicResource DetailControlTemplate}"
ContentTemplate="{DynamicResource DetailDataTemplate}"/>
And your ControlTemplate is missing a TargetType:
<ControlTemplate x:Key="DetailControlTemplate" TargetType="ContentControl">
<Border Style="{StaticResource ObjectDetailBorderStyle}">
<ContentPresenter/>
</Border>
</ControlTemplate>
Without the TargetType, the ContentPresenter's properties aren't set by default, and you would have to set them explicitly like
<ControlTemplate x:Key="DetailControlTemplate">
<Border Style="{StaticResource ObjectDetailBorderStyle}">
<ContentPresenter
Content="{Binding Content,
RelativeSource={RelativeSource TemplatedParent}}"
ContentTemplate="{Binding ContentTemplate,
RelativeSource={RelativeSource TemplatedParent}}"/>
</Border>
</ControlTemplate>

Apply style to first child?

Is there some way to apply styles to the first (or last or nth) child of a container (anything that contains children)? I am trying to customize the look of tab items so that the first one has different border radius than the others.
This is what I have now:
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid>
<Border Name="Border" BorderBrush="#666" BorderThickness="1,1,1,0" CornerRadius="8,8,0,0" Margin="0,0,0,-1">
<TextBlock x:Name="TabItemText" Foreground="#444" Padding="6 2" TextOptions.TextFormattingMode="Display">
<ContentPresenter x:Name="ContentSite" VerticalAlignment="Center" HorizontalAlignment="Center" ContentSource="Header" Margin="12,2,12,2"/>
</TextBlock>
</Border>
</Grid>
</ControlTemplate>
For ItemsControl derived classes (such as TabControl), you can use the ItemContainerStyleSelector dependency property. When this dependency property is set, ItemsControl will call StyleSelector.SelectStyle() for each item in the control. This will allow you to use different styles for different items.
The following example changes the last tab item in a TabControl so its text is bold and a bit larger than the other tabs.
First, the new StyleSelector class:
class LastItemStyleSelector : StyleSelector
{
public override Style SelectStyle(object item, DependencyObject container)
{
var itemsControl = ItemsControl.ItemsControlFromItemContainer(container);
var index = itemsControl.ItemContainerGenerator.IndexFromContainer(container);
if (index == itemsControl.Items.Count - 1)
{
return (Style)itemsControl.FindResource("LastItemStyle");
}
return base.SelectStyle(item, container);
}
}
This style selector will return the style with the key "LastItemStyle" but only for the last item in the control. The other items will use the default style. (Note, that this function only uses members from ItemsControl. It could also be used for other ItemsControl derived classes.) Next, in your XAML, you first need to create two resources. The first resource will be to this LastItemStyleSelector and the second resource is the style.
<Window.Resources>
<local:LastItemStyleSelector x:Key="LastItemStyleSelector" />
<Style x:Key="LastItemStyle" TargetType="TabItem">
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="FontSize" Value="16" />
</Style>
</Window.Resources>
Then finally your TabControl:
<TabControl ItemContainerStyleSelector="{StaticResource LastItemStyleSelector}">
<TabItem Header="First" />
<TabItem Header="Second" />
<TabItem Header="Third" />
</TabControl>
For more information see the MSDN documentation:
ItemsControl.ItemContainerStyleSelector Property
StyleSelector Class
Unlike HTML and CSS, there's not a simple way to determine and trigger that type of change.
You could potentially write a trigger and use a value converter to do something like that using this forum post as inspiration potentially.
Much simpler would be to apply a custom style to the tabitem that you want to look different. Have you tried that?
<TabItem Header="TabItem" Style="{DynamicResource FirstTabStyle}">
<Grid Background="#FFE5E5E5"/>
</TabItem>

Categories