ContentPresenter with ContentTemplateSelector within DataContext = NULL context? - c#

I have a Windows Store application that ueses datatemplates and datatemplateselectors to style various content for display.
I have a TemplateSelector that returns a Template based an parameters. This template also uses a contentpresenter which itself has a TemplateSelector.
I have tried calling the inner TemplateSelector in this ways:
<DataTemplate x:Key="Template1">
<Grid>
<Border Background="{StaticResource ApplicationBackgroundBrush}">
<Grid>
<ContentPresenter Content="{TemplateBinding DataContext}">
<ContentPresenter.ContentTemplateSelector>
<my:MyTemplateSelector />
</ContentPresenter.ContentTemplateSelector>
</ContentPresenter>
</Grid>
</Border>
</Grid>
Using the TemplateBinding, i get an exception that the DataContext Property cannot be found (DependencyProperty cannot be created from string).
My first attempt was this:
<DataTemplate x:Key="Template2">
<Grid>
<Border Background="{StaticResource ApplicationBackgroundBrush}">
<Grid>
<ContentPresenter Content="{Binding DataContext}">
<ContentPresenter.ContentTemplateSelector>
<my:MyTemplateSelector />
</ContentPresenter.ContentTemplateSelector>
</ContentPresenter>
</Grid>
</Border>
</Grid>
The problem with this is that the Content and DataContext are null when the TemplateSelector gets called. How can i solve this problem?

Try this, I had a similar problem because I missed the Property Keyword. Hope it applies to your situation.
Content={TemplateBinding Property=DataContext}

It works if you use ContentControl instead of ContentPresenter.

Related

Combining DataTemplates at runtime

I have a ListBox that presents a databound list of objects via its ItemSource. Because each object has special display needs I’m defining an ItemTemplateSelector that returns the appropriate DataTemplate depending on the object. That all works without a hitch.
The DataTemplates for each object follow a common formula, but contains custom elements in the middle. For example:
<DataTemplate x:Key="collectibleTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border BorderBrush="LightGray" BorderThickness="1">
<Expander IsExpanded="True" Header="{Binding ComponentName}" Background="WhiteSmoke">
<StackPanel>
<TextBlock Margin="5,5,5,0" Text="{Binding EditDescription}" TextWrapping="Wrap" />
<!-- This is the only custom part of each template -->
<StackPanel Margin="0,10,5,0" Orientation="Horizontal">
<Label Content="Type:" />
<ComboBox Height="22" HorizontalAlignment="Left" SelectedItem="{Binding Path=CollectibleType, Mode=TwoWay}"
ItemsSource="{Binding Source={StaticResource collectibleTypeFromEnum}}" />
</StackPanel>
<!-- End custom part -->
<StackPanel Margin="0,0,0,5">
<Label Content="Available Actions:" >
<Label.Style>
<Style TargetType="Label">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding EditActions.Count}" Value="0">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<ItemsControl ItemsSource="{Binding EditActions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding}" Content="{Binding Title}" ToolTip="{Binding ToolTip}" Margin="5,0,5,0"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</Expander>
</Border>
</Grid>
</DataTemplate>
As you can see there’s lots of shared XAML, wrapping a small custom section in the middle.
Additional data templates will be written by other engineers (they’ll want to create one for each new object type that they add), so I’m interested in making the creation of a new DataTemplate as fool-proof and painless as possible. No copying of the entire DataTemplate with the custom “stuff” added in the middle, of course – but I’m also not partial to extracting parts of the template as reusable parts and referencing them in because it still leads to lots of duplicate code in each new DataTemplate, and that means possible errors and hard maintainability. I.e., this right here is a more maintainable approach but still feels suboptimal:
<DataTemplate x:Key="collectibleTemplate">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border BorderBrush="LightGray" BorderThickness="1">
<Expander IsExpanded="True" Header="{Binding ComponentName}" Background="WhiteSmoke">
<StackPanel>
<TextBlock Margin="5,5,5,0" Text="{Binding EditDescription}" TextWrapping="Wrap" />
<!-- This is the only custom part of each template -->
[...]
<!-- End custom part -->
<ContentPresenter Content="{StaticResource AvailableActions}" />
</StackPanel>
</Expander>
</Border>
</Grid>
</DataTemplate>
<StackPanel Margin="0,0,0,5" x:Key="AvailableActions" x:Shared="false">
<Label Content="Available Actions:" >
<Label.Style>
<!--
[Bottom half of shared XAML from the first example, offloaded here]
-->
</StackPanel>
So: what is my best strategy to solve this? AFAIK I’m stuck with using DataTemplates because that’s the only element that a ListBox ItemTemplateSelector accepts. Is there a way to create a compound DataTemplate in the DataTemplateSelector? I'd provide the stock DataTemplate that is shared by all objects, and the DataTemplateSelector references in the bit of custom XAML needed for each object type. Other engineers would hook into that generalized code behavior.
Not sure, fumbling a bit in the dark here as whether there is a pattern that allows me to solve this elegantly.
And, just for reference: my current DataTemplateSelector is super straightforward. This is where I would expect to construct the final DataTemplate, rather than simply returning one that's hardcoded in XAML.
public class NodeComponentDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
FrameworkElement element = container as FrameworkElement;
if (element != null && item != null)
{
if (item is CollectibleComponent)
return element.FindResource("collectibleTemplate") as DataTemplate;
// [...]
}
}
}
You could create the DataTemplate dynamically using the XamlReader.Parse or XamlReader.Load method, e.g.:
string template = "<DataTemplate xmlns =\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\" xmlns:x =\"http://schemas.microsoft.com/winfx/2006/xaml\"><StackPanel>[PLACEHOLDER]</StackPanel></DataTemplate>".Replace("[PLACEHOLDER]", "...custom code...");
return System.Windows.Markup.XamlReader.Parse(template) as DataTemplate;
The custom parts could be defined as UserControls.
I am afraid there is no way to base a DataTemplate on another one in pure XAML though.
You could create a new CustomControl that fits your needs. It will apply the style by itself and you can give additional DepdendencyProperties to make it more convinient. In the end you can still put it in a DataTemplate to use it with your DataTemplateSelector.

In a WPF control that acts as a container, how do I place the content?

I am writing a WPF control that is meant to be a container in the same way Border and ScrollViewer are containers. It is called EllipsisButtonControl, and it is supposed to place an ellipsis button to the right of its content. Here's an example of how I intend for it to be used:
<local:EllipsisButtonControl>
<TextBlock Text="Testing" />
</local:EllipsisButtonControl>
Here is the XAML for EllipsisButtonControl:
<ContentControl
x:Class="WpfApplication1.EllipsisButtonControl"
x:Name="ContentControl"
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"
d:DesignHeight="30" d:DesignWidth="300">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0" Content="{Binding ElementName=ContentControl, Path=Content}" />
<Button Grid.Column="1" Command="{Binding ElementName=ContentControl, Path=Command}" Margin="3,0" Width="30" Height="24" MaxHeight="24" VerticalAlignment="Stretch" Content="..." />
</Grid>
</ContentControl>
And here is the code behind:
using System.Windows;
using System.Windows.Input;
namespace WpfApplication1
{
public partial class EllipsisButtonControl
{
public EllipsisButtonControl()
{
InitializeComponent();
}
public static string GetCommand(DependencyObject obj)
{
return (string)obj.GetValue(CommandProperty);
}
public static void SetCommand(DependencyObject obj, string value)
{
obj.SetValue(CommandProperty, value);
}
public static readonly DependencyProperty CommandProperty = DependencyProperty.RegisterAttached(
name: "Command",
propertyType: typeof(ICommand),
ownerType: typeof(EllipsisButtonControl),
defaultMetadata: new UIPropertyMetadata());
}
}
This doesn't work. It crashes the Designer with a System.Runtime.Remoting.RemotingException.
I believe the binding on the ContentPresenter of the EllipsisButtonControl XAML is wrong, but I don't know how to make it right. What is the appropriate syntax to make that line reference the control's content? (e.g. The TextBlock defined in the usage example)
Edit:
poke provided a comprehensive answer below (including working code), but for the benefit of others who might share my initial misunderstanding, let me summarize the key concept here: A container control cannot "place content", per se. It achieves the desired effect by defining a template that modifies the way the calling XAML presents the content. The rest of the solution follows from that premise.
<local:EllipsisButtonControl>
<TextBlock Text="Testing" />
</local:EllipsisButtonControl>
This does set the Content of your user control. But so does the following in the user control’s XAML:
<ContentControl …>
<Grid>
…
</Grid>
</ContentControl>
The calling XAML has precendence here, so whatever you do inside that user control’s XAML is actually ignored.
The solution here is to set the template of the user control. The template, in this case the control’s control template, determines how the control itself is rendered. The simplest template for a user control (and also its default) is just using a ContentPresenter there, but of course, you want to add some stuff around that, so we have to overwrite the template. This generally looks like this:
<ContentControl …>
<!-- We are setting the `Template` property -->
<ContentControl.Template>
<!-- The template value is of type `ControlTemplate` and we should
also set the target type properly so binding paths can be resolved -->
<ControlTemplate>
<!-- This is where your control code actually goes -->
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
Now this is the frame you need to make this work. However, once you’re inside the control template, you need to use the proper binding type. Since we are writing a template and want to bind to properties of the parent control, we need to specify the parent control as the relative source in bindings. But the easiest way to do that is to just use the TemplateBinding markup extension. Using that, a ContentPresenter can be placed like this inside the ControlTemplate above:
<ContentPresenter Content="{TemplateBinding Content}" />
And that should be all you need here in order to get the content presenter working.
However, now that you use a control template, of course you need to adjust your other bindings too. In particular the binding to your custom dependency property Command. This would generally look just the same as the template binding to Content but since our control template is targetting the type ContentControl and a ContentControl does not have your custom property, we need to explicitly reference your custom dependency property here:
<Button Command="{TemplateBinding local:EllipsisButtonControl.Command}" … />
Once we have that, all the bindings should work fine. (In case you are wondering now: Yes, the binding always targets the static dependency property on the type)
So, to sum this all up, your custom content control should look something like this:
<ContentControl
x:Class="WpfApplication1.EllipsisButtonControl"
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"
xmlns:local="clr-namespace:WpfApplication1"
d:DesignHeight="30" d:DesignWidth="300" mc:Ignorable="d">
<ContentControl.Template>
<ControlTemplate TargetType="ContentControl">
<Grid>
<ContentPresenter Grid.Column="0"
Content="{TemplateBinding Content}" />
<Button Grid.Column="1" Content="…"
Command="{TemplateBinding local:EllipsisButtonControl.Command}" />
</Grid>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>
Try replacing this line:
<ContentPresenter Grid.Column="0" Content="{Binding ElementName=ContentControl, Path=Content}" />
With this
<ContentPresenter Grid.Column="0" Content={Binding Content} />
In your existing code you are making this ContentPresenter display the generated content of EllipsesButtonControl, which includes the ContentPresenter which must render the generated content of ElipsesButtonControl which includes the ContentPresenter..... Unlimited recursion.
The XAML of your EllipsisButtonControl already sets its Content to the top-level Grid. What you probably wanted is to create a ControlTemplate, e.g. like this:
<ContentControl x:Class="WpfApplication1.EllipsisButtonControl"
x:Name="ContentControl"
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" d:DesignHeight="30" d:DesignWidth="300">
<ContentControl.Template>
<ControlTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
Content="{Binding ElementName=ContentControl, Path=Content}"/>
<Button Grid.Column="1"
Command="{Binding ElementName=ContentControl, Path=Command}"
Margin="3,0" Width="30" Height="24" MaxHeight="24"
VerticalAlignment="Stretch" Content="..." />
</Grid>
</ControlTemplate>
</ContentControl.Template>
</ContentControl>

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>

My listview items are not showing up when setting a template for it

I have two templates, one for a textbox and one for a listview, both are just used to give them rounded corners instead of the default rectangle. My textbox needed the "ScrollViewer x:Name="PART_ContentHost" line in order to show text, but that doens't work for the listview. If I take out the Template for the listview the example listviewitem (stuff) will show up. Otherwise it won't and I can't see any other items I add in the code behind. What am I missing in the xaml to get this to work?
Here is my xaml below:
<!-- Design Templates to set the borders of the controls-->
<UserControl.Resources>
<ControlTemplate x:Key="TextBoxTemplate" TargetType="TextBox">
<Border BorderBrush="Black" BorderThickness="1,1,1,.5" CornerRadius="7">
<ScrollViewer x:Name="PART_ContentHost" ></ScrollViewer>
</Border>
</ControlTemplate>
<ControlTemplate x:Key="ListViewTemplate" TargetType="ListView">
<Border BorderBrush="Black" BorderThickness=".5,1,1,1" CornerRadius="7">
</Border>
</ControlTemplate>
</UserControl.Resources>
<!-- Controls -->
<Grid Height="270" Width="400">
<StackPanel Width="390">
<TextBox Height="35" Name="InputTextbox" Template="{StaticResource TextBoxTemplate}" VerticalContentAlignment="Center" TextChanged="InputTextbox_TextChanged"></TextBox>
<ListView Height="235" Name="ResultsListView" Template="{StaticResource ListViewTemplate}" SelectionChanged="ResultsListView_SelectionChanged">
<ListViewItem Content="stuff"></ListViewItem>
</ListView>
</StackPanel>
</Grid>
Your ControlTemplate doesn't have a presenter associated with it. This is why you're not seeing any items. See this page for a working example of how to create a ListView.ControlTemplate:
MSDN: ListView ControlTemplate Example
and here's an updated xaml for your control template:
<ControlTemplate x:Key="ListViewTemplate" TargetType="ListView">
<Border BorderBrush="Black" BorderThickness=".5,1,1,1" CornerRadius="7">
<ScrollViewer>
<ItemsPresenter />
</ScrollViewer>
</Border>
</ControlTemplate>

How to access and provide default values to controls within WPF templates?

I'm struggling with control templates. I'm currently building a UI which has several panes which are essentially build out of more basic controls.
here's how one of our construction panes looks like right now:
<Grid>
<StackPanel>
<ContentControl Template="{StaticResource ConstructionBorderCtrl}">
<ContentControl Template="{StaticResource StringCtrl}" Content="Cash Event Value:"/>
</ContentControl>
<ContentControl Template="{StaticResource ConstructionBorderCtrl}">
<ContentControl Template="{StaticResource RateCtrl}"></ContentControl>
</ContentControl>
<ContentControl Template="{StaticResource ConstructionBorderCtrl}">
<ContentControl Grid.Row="0" Template="{StaticResource FromCtrl}"></ContentControl>
</ContentControl>
<ContentControl Template="{StaticResource ConstructionBorderCtrl}">
<ContentControl Grid.Row="0" Template="{StaticResource StartEndDateCtrl}"></ContentControl>
</ContentControl>
<ContentControl Template="{StaticResource ConstructionBorderCtrl}">
<ContentControl Grid.Row="0" Template="{StaticResource ComboStringCtrl}">Applicable Size:</ContentControl>
</ContentControl>
</StackPanel>
</Grid>
Here's a template for the StringCtrl as an example:
<ControlTemplate x:Key="StringCtrl" TargetType="ContentControl">
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Name="ctrlText" Margin="0,0,5,0" Text="{TemplateBinding Content}"></TextBlock>
<TextBox Name="ctrlDefaultValue" Grid.Column="1" />
</Grid>
</ControlTemplate>
As you can see from the template it's really just a label and a textbox. Now let's say I wanted to provide a default value to the text box as well as perform validation on user input, but I want to provide that context from the parent Construction pane and bind it to the individual elements inside the templates. How would I go about doing that?
This is certainly a design I've never seen before. I'd think UserControls or some other type of custom control would work better for this than the ControlTemplate approach.
But if you definitely want to go down this route, I could maybe see a Behavior working for you if there's some consistency to your structure/naming in the templates - you can set properties on the behavior and the behavior can access the control via its AssociatedObject property to be able to set the values of the children and do validation.
Seems like a lot of work to me though.

Categories