WPF custom control and direct content support - wpf

I am fairly new to WPF, and am a bit stuck, so any help would be appreciated.
I am trying to write WPF custom control that encapsulates several elements of functionality that I already having working (i.e sorting, filtering, standard menus, etc.), but in a nice neat package to avoid repetition.
Anyway I have created the custom control (based on control), and then have the following in the Generic.Xaml
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Controls.ListViewExtended">
<Style TargetType="{x:Type local:ListViewExtended}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:ListViewExtended}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ListView>
<ListView.View>
<GridView>
<!-- Content goes here -->
</GridView>
</ListView.View>
</ListView>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
When I try to add GridViewColumns (or any control really), as below ...
<elv:ListViewExtended>
<GridView>
<GridViewColumn Width="140" Header="Column 1" />
<GridViewColumn Width="140" Header="Column 2" />
<GridViewColumn Width="140" Header="Column 3" />
</GridView>
</elv:ListViewExtended>
I get the "... does not support direct content" error.
I have created a dependancy property (again below) that allows the adding of GridView, but it still doesn't work.
public static DependencyProperty GridViewProperty;
public static string GridViewHeader(DependencyObject target)
{
return (string)target.GetValue(GridViewProperty);
}
public static void GridViewHeader(DependencyObject target, string value)
{
target.SetValue(GridViewProperty, value);
}
Thanks in advance

All You need is to specify ContentPropertyAttribute.
[ContentProperty("MainContent")]
public class GroupPanel : Control
{
public GroupPanel()
{
DefaultStyleKey = typeof(GroupPanel);
}
public object MainContent
{
get { return GetValue(MainContentProperty); }
set { SetValue(MainContentProperty, value); }
}
public static readonly DependencyProperty MainContentProperty =
DependencyProperty.Register("MainContent", typeof(object), typeof(GroupPanel), null);
}
Reference: http://msdn.microsoft.com/en-us/library/system.windows.markup.contentpropertyattribute(v=vs.90).aspx

I use this simple solution in our projects to support direct content in custom controls:
Add a "CustomControl" to your project and derive this control from class "UserControl" instead of "Control":
public class MyCustomControl: UserControl
{
static MyCustomControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyCustomControl),
new FrameworkPropertyMetadata(typeof(MyCustomControl)));
}
}
When you add a CustomControl to your project, Visual Studio (I am using 2012) automatically adds a folder "Themes" including a file named "Generic.xaml".
This file holds a ResourceDictionary to define the style (template) of your CustomControl.
You will find a basic template for your CustomControl, already used as DefaultStyle. For direct content support place a ContentPresenter somewhere inside this template with parent content binding:
<Style TargetType="{x:Type local:MyCustomControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyCustomControl}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Content}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Now it's possible to add content to your CustomControl:
<Window x:Class="MyApplication.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:MyApplication"
Title="MainWindow" Height="350" Width="525">
<Grid>
<controls:MyCustomControl>
<TextBlock>Hello</TextBlock>
</controls:MyCustomControl>
</Grid>
</Window>
Hope this helps!

Inherit your custom control from the ListView, not from the Control. This would definitely cause you to change the template, but I encourage you to read more documentation on how to do it (e.g. Sacha Barber's article: Creating and consuming a custom WPF control).
Good luck in your learning!

Don't use
{Binding MainContent ElementName=Self}
Use
Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=MainContent}" />
Could have saved me so much time with that.

Related

How can I have recursive Data Template for my Custom Control

I have a Custom Control, which is basically a kind of a TreeView. Now the thing is I need to have any level of detail in my TreeView control so I came up with the following data template
I have the following Generic.xaml
<DataTemplate x:Key="treetemplate">
<StackPanel>
<TextBlock Text="{Binding Label}" ></TextBlock>
<ItemsControl ItemsSource="{Binding Children}" ItemTemplate="{DynamicResource treetemplate}" />
</StackPanel>
</DataTemplate>
<ControlTemplate TargetType="{x:Type local:CustomControl1}" x:Key="testkey">
<ControlTemplate.Resources>
<local:AnythingToListConverter x:Key="anyconv"></local:AnythingToListConverter>
</ControlTemplate.Resources>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ItemsControl ItemsSource="{Binding Root, Converter={StaticResource anyconv}}" ItemTemplate="{StaticResource treetemplate}" />
</Border>
</ControlTemplate>
<Style TargetType="{x:Type local:CustomControl1}">
<Setter Property="Template" Value="{StaticResource testkey}" />
</Style>
Here is my custom control constructor
this.Root = new Node();
this.Root.Label = "Root";
this.Root.Children = new List<Node>();
this.Root.Children.Add(new Node(){Label="Child1"});
this.DataContext = this;
And this is how control looks like
Here is what I think the problem is
For the Recursive call of same template, I am using DynamicResource. Which never worked on my and the actual resource never got called. If I change that to StaticResource, it will not compile, because it won't see itself. How do I fix it ?
Full solution can be downloaded here.
What you need is HierarchicalDataTemplate. It's designed to pinpoint exactly this scenario.
Here is how you use it:
<HierarchicalDataTemplate ItemsSource="{Binding Children}"
DataType="{x:Type local:Node}">
<TextBlock Text="{Binding Label}" ></TextBlock>
</HierarchicalDataTemplate>
Note that using this template, we have to set its DataType to the Type of your data item (in this case I guess it's Node). Also I guess you don't need any custom control for this, just use the default TreeView with this template defined as some resource like this:
<TreeView ItemsSource="{Binding Root, Converter={StaticResource anyconv}}">
</TreeView>
If you still want to keep your code, try just replacing your ItemsControl with TreeView or some HeaderedItemsControl. The HierarchicalDataTemplate is used only for HeaderedItemsControl (TreeView is also just a kind of HeaderedItemsControl).

In WPF, how do I override style set by the template at the instance level?

So I have a custom template for scrollbar. It work great, but I would like to override the corner rounding value. On the template, it is set like this:
<ControlTemplate x:Key="VerticalScroll" TargetType="{x:Type ScrollBar}">
<Grid>
<Border Grid.RowSpan="3" CornerRadius="3" BorderBrush="DarkBlue" BorderThickness="1" Opacity=".6"></Border>
I am creating my instance like this:
<ScrollViewer Padding="0,0,0,0">
<TextBlock Height="23" HorizontalAlignment="Stretch" Margin="0" Name="textBlock1" Text="TextBlock" VerticalAlignment="Top" />
</ScrollViewer>
How do I change the CornerRadius to 6?
I don't think there's any easy way to do this, since there is no template inheritance in WPF. You would either have to copy your entire template and modify it for this particular instance, or set the template in Styles
Use an Attached DependencyProperty. The Attached DependencyProperty allows you to store information and against a DependencyObject.
So, create an Attached DependencyProperty, in the following case, against the MainWindow.
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.RegisterAttached("CornerRadius", typeof(int), typeof(MainWindow));
public static void SetCornerRadius(DependencyObject element, int value)
{
element.SetValue(CornerRadiusProperty, value);
}
public static int GetCornerRadius(DependencyObject element)
{
return (int) element.GetValue(CornerRadiusProperty);
}
Then in the Xaml, on the ScrollViewer assign the value
<ScrollViewer Padding="0,0,0,0" this:MainWindow.CornerRadius="20" ... >
and in the template, reference the Attached DependencyProperty using a TemplatedParent binding.
<ControlTemplate x:Key="ScrollViewerTemplate" TargetType="{x:Type ScrollViewer}">
<Grid>
<Border Grid.RowSpan="3" CornerRadius="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(this:MainWindow.CornerRadius)}" BorderBrush="DarkBlue" BorderThickness="1" Opacity=".6">
I hope this helps.
In case its only property you interested to set from outside, you can store value in Tag and do TemplateBinding with Tag.
<Border CornerRadius="{TemplateBinding Tag}"/>
and in instance ScrollViewer:
<ScrollViewer Tag="6"/>
However, if there are more properties you want to set from outside, either subclass ScrollViewer and create your custom DP's or you can also go with attached properties in case not interested in subclassing of ScrollViewer.

Partially templated ListBox.ItemTemplate

I'm creating a custom control and I'm trying to create partially specified template for list box items. The template has some predefined parts and there should be another part that can be templated when using the control.
For this I have created a dependency property named SuggestionItemTemplate like so:
public static readonly DependencyProperty SuggestionItemTemplateProperty =
DependencyProperty.Register("SuggestionItemTemplate",
typeof(DataTemplate),
typeof(AutoSuggestTextBox),
new PropertyMetadata(null));
In my custom controls' generic.xaml I have:
<Style TargetType="local:AutoSuggestTextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:AutoSuggestTextBox">
<Grid>
<ListBox x:Name="ItemsControl">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
ContentTemplate="{TemplateBinding SuggestionItemTemplate}"
Content="{Binding}" />
<ToggleButton Grid.Column="1"
x:Name="DetailsHover"
ClickMode="Hover"
Style="{StaticResource DetailsToggleButtonStyle}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Unfortunatelly, this does not work as it's not possible to use TemplateBinding from inside ContentPresenter nested into DataTemplate. (The member "SuggestionItemTemplate" is not recognized or is not accessible.)
I also tried to use ancestor binding (available in Silverlight 5) like:
<ContentPresenter Grid.Column="0"
ContentTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:AutoSuggestTextBox}, Path=SuggestionItemTemplate}"
Content="{Binding}" />
But this results in binding error:
Error: System.Exception: BindingExpression_CannotFindAncestor
I suppose this happens because I'm inside ControlTemplate of my custom control and "local:AutoSuggestTextBox" is not defined anywhere in the style.
The third option that I tried was to apply ContentTemplate in OnApplyTemplate override but this also doesn't work:
var cp = itemsControlElement.ItemTemplate.LoadContent() as ContentPresenter;
cp.ContentTemplate = SuggestionItemTemplate;
In all cases, I get my grid with two columns, toggle button is visible but content presenter simple prints out view model's type name. (I believe this is the default behavior if the ContentTemplate is null).
Is this even possible to do? Are there any other ways to specify a partial template and then only add customized template part when necessary?
As a workaround for now, I can specify
ItemTemplate="{TemplateBinding SuggestionItemTemplate}"
for the list box and then copy/paste the generic template everywhere I use this control. But this is the behavior I'm hoping to avoid in the first place.
Thanks!
edit: I used the code tags for all blocks of code, but they're not highlighted for some reason. :/
It is possible to walk through Visual Ancestors in the OnApplyTemplate method, find your ContentPresenter(s) and set the ItemTemplate on that. To my mind, this is fine for a single item, but not so much in an ItemsControl scenario.
You could achieve what you are after using your own custom Control. Just give it a Content dependency property of type Object, and a Template DP of type DataTemplate (and multiples of the two if you fancy), and you can set up the root visual style and templates in the default style for your Control.
In this specific case, I would suggest that the best approach is to put your ToggleButton in the ListBoxItem template instead by customising the ListBox.ItemContainerStyle. It is easy to modify the default Control Template using Expression Blend, and the DataContext of the ToggleButton will not change, so the changes to your own logic should be minimal.
Edit: If you mean to use a number of different data templates, perhaps Implicit Data Templates will be more suitable.
I managed to solve this using a different approach. I used ancestor binding but instead of trying to reach the root control (my AutoSuggestTextBox) from the DataTemplate, I ask for a reference to my ListBox (here named ItemsControl).
However, since the ListBox doesn't have the SuggestionItemTemplate property, I sub-classed it to my own CustomListBox where I implemented that property. It all comes down to this code snippet:
<Style TargetType="local:AutoSuggestTextBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:AutoSuggestTextBox">
<Grid>
<local:CustomizableListBox x:Name="ItemsControl"
SuggestionItemTemplate="{TemplateBinding SuggestionItemTemplate}">
<local:CustomizableListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
ContentTemplate="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=local:CustomizableListBox}, Path=SuggestionItemTemplate}"
Content="{Binding}" />
<ToggleButton Grid.Column="1"
x:Name="DetailsHover"
ClickMode="Hover"
Style="{StaticResource DetailsToggleButtonStyle}" />
</Grid>
</DataTemplate>
</local:CustomizableListBox.ItemTemplate>
</local:CustomizableListBox>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

Silverlight GetTemplateChild of control within control returns null

I'm trying to add an indicator (I'm using TextBlock) to the datepicker control.
Visually it works but I can't get the control via GetTemplateChild. I assume it's something to do with the fact that the TextBlock control I added is in the DatePickerTextBox style template as opposed to the DatePicker style template.
I've tried DefaultStyleKey (although I don't think this makes sense as it's the TextBox control within DatePicker that's the problem) and using OnApplyTemplate and UpdateLayout on the TextBox control.
Here's a snippet of the Dictionary.xaml
<Style x:Key="Ind_DatePickerTextBoxStyle" TargetType="primitives:DatePickerTextBox">
...
<Grid VerticalAlignment="Stretch">
<TextBlock x:Name="Indicator" Text="*" Style="{StaticResource IndicatorStyle}" Visibility="Collapsed"/>
...
<!--datepicker style snippet-->
<primitives:BF_DatePickerTextBox
x:Name="TextBox"
SelectionBackground="{TemplateBinding SelectionBackground}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
Grid.Column="0"
Style="{StaticResource Ind_DatePickerTextBoxStyle}" />
GetTemplateChild can only be used from "your control" to get a control defined in it's [control]template. When you have defined a control and given it a style you can use GetTemplateChild
public class MyCustomControl : Control
{
override OnApplyTemplate()
{
var textbox = GetTemplateChild("TextBox");
}
}
<Style TargetType="local:MyCustomControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:MyCustomControl">
<TextBox x:Name="TextBox"/>
</ControlTemplate>
</Setter.Value>
</Setter>
So in this example I was able to use GetTemplateChild to get the TextBox child inside the control because I was accesings my [control]template. I cannot use GetTemplateChild to get the TextBox from another control that uses MyCustomControl. Only MyCustomControl can use GetTemplateChild to get the TextBox.
Now I can extend MyCustomControl to do the same
public class MyOtherCustomControl : MyCustomControl
{
override OnApplyTemplate()
{
var textbox = GetTemplateChild("TextBox");
}
}
I hope this helps!

How to hide the navigation bar in a WPF page

I want to hide the navigation bar in a page created using WPF. I have tried ShowsNavigationUI = false, but it is still displaying the control.
Specify to the page Container, the intention to not have a navigation bar, using
NavigationUIVisibility property.
<Frame NavigationUIVisibility="Hidden" Panel.ZIndex="1" ... />
It's a very easy implementation.
<Frame x:Name="_FrameName" NavigationUIVisibility="Hidden" />
Setting ShowsNavigationUI=False on a Page ought to do it. There does seem to be a bug, however, that will cause this to fail in at least one sequence of events:
Page is already in NavigationWindow when this is set
Page is navigated away and back again
There may be other scenarios I haven't run into yet that make it fail.
To get this to work totally reliably, what I do is ignore the Page.ShowsNavigationUI property entirely and set it instead on NavigationWindow. This seems to be completely reliable.
Here is how this can be done in your Page constructor:
Dispatcher.BeginInvoke(ApplicationPriority.Render, new Action(() =>
{
var navWindow = Window.GetWindow(this) as NavigationWindow;
if(navWindow!=null) navWindow.ShowsNavigationUI = false;
}));
If you do this, remember not to set ShowsNavigationUI on any Page object.
FYI, you can also restyle your NavigationWindow any way you like by changing its ControlTemplate. For example this removes everything but the actual page content:
<Style TargetType="{x:Type NavigationWindow}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type NavigationWindow}">
<AdornerDecorator>
<ContentPresenter Name="PART_NavWinCP"
ClipToBounds="true"/>
</AdornerDecorator>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
If you're using a Frame you can change the Frame's default style to remove the navigation buttons (shown below). The same approach could be done for NavigationWindow. I originally tried setting Page.ShowsNavigationUI and it had no effect. Just add the below style to a ResourceDictionary and it works fine.
<Style TargetType="{x:Type Frame}">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Frame}">
<Border BorderThickness="{TemplateBinding Border.BorderThickness}" Padding="{TemplateBinding Control.Padding}" BorderBrush="{TemplateBinding Border.BorderBrush}" Background="{TemplateBinding Panel.Background}">
<ContentPresenter Content="{TemplateBinding ContentControl.Content}" ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}" ContentStringFormat="{TemplateBinding ContentControl.ContentStringFormat}" Name="PART_FrameCP" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This one I found really easy. In your MainWindow, do this:
public MainWindow()
public partial class MainWindow : NavigationWindow
{
public MainWindow()
{
InitializeComponent();
ShowsNavigationUI = false;
}
}
}
And if you have an event on button click to open a new page, just do this:
private void btnEndUserSearch_Click(object sender, RoutedEventArgs e)
{
EndUser EndUserSearchPage = new EndUser();
this.NavigationService.Navigate(EndUserSearchPage);
EndUserSearchPage.ShowsNavigationUI = false;
}
Above works only for Navigation windows, but I am using ordinary WPF windows. Some say these are better than Navigation windows. I am using DockPanel to host my pages. My solution creates a new template for the DockPanel and simply does not add buttons or makes them hidden (see StackPanel Visibility="Hidden"). It works nicely.
<DockPanel>
<Frame x:Name="_mainFrame">
<Frame.Template>
<ControlTemplate TargetType="Frame">
<DockPanel Margin="7">
<StackPanel Visibility="Hidden"
Margin="0"
Orientation="Horizontal"
DockPanel.Dock="Top"
>
<!--<Button
Content="Avast! Go back!"
Command="{x:Static NavigationCommands.BrowseBack}"
IsEnabled="{TemplateBinding CanGoBack}"
/>
<Button
Content="Forward you dogs!"
Command="{x:Static NavigationCommands.BrowseForward}"
IsEnabled="{TemplateBinding CanGoForward}"
/>-->
</StackPanel>
<Border>
<ContentPresenter />
</Border>
</DockPanel>
</ControlTemplate>
</Frame.Template>
</Frame>
</DockPanel>
One of the easiest and simplest way is to add:
ShowsNavigationUI = false;
in your cs file constructor under InitializeComponent();
For beginners, you can simply change code at MainWindow.xaml like this.
<Window>
<Grid >
<Frame x:Name="LeftFrame" NavigationUIVisibility="Hidden"/>
<Frame x:Name="RightFrame" NavigationUIVisibility="Hidden"/>
</Grid>
</Window>
I had this problem whenever I dynamically changed the Content property of a Frame, and solved it by using the following code in my click() event.
ContentFrame.NavigationUIVisibility = NavigationUIVisibility.Hidden;
Where ContentFrame is the name of the frame, as defined in XAML. i.e.
<Frame x:Name="ContentFrame" />
On the NavigationWindow itself I use ShowsNavigationUI="False"

Resources