Reference to control inside a ControlTemplate - c#

How do I, form my contructor in the code-behind get a reference to the OuterBorder control in the XAML below?
<Window Template="{DynamicResource WindowTemplate}">
<Window.Resources>
<ControlTemplate x:Key="WindowTemplate" TargetType="{x:Type Window}">
<AdornerDecorator>
<Border Name="OuterBorder" Background="Black" BorderBrush="Red" BorderThickness="1" CornerRadius="0">
<!-- Implementation here... -->
</Border>
</AdornerDecorator>
</ControlTemplate>
</Window.Resources>
</Window>

Two possible solutions:
Solution 1
Put a Loaded event in XAML
<Border Name="OuterBorder" Loaded="Border_Loaded" ...
And in code behind store it in a private field:
private Border border;
void Border_Loaded(object sender, RoutedEventArgs e)
{
this.border = (Border)sender;
}
OR:
Solution 2
Override the OnApplyTemplate of your Window:
private Border border;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.border = (Border) Template.FindName("OuterBorder", this);
}

You may want to reconsider your approach. What are you trying to do?
Generally, you shouldn't want or need to access portions of the ControlTemplate from your codebehind because your template is just that-- a template. It's how the control looks. You want your codebehind to generally affect the behavior of the control.
For example, if you're trying to affect the color of the border in the codebehind in certain interactive situations, you really want to add some (pre .Net4) triggers or (post .Net4) a VisualStateManager to your control template to manage your control's visual states for you.

Related

WPF Child Control Inheritance

I am trying to implement a control to inherit from in WPF.
I have never been working with WPF (at least at that level though).
So I need some direction of best practice on how to solve this.
The problem I´m facing is that my control, that I want to inherit from, has some child controls that need to be accessed inside the controls base class.
I want to reuse that control with these child controls inside, because it has functions to fill the child controls from outside.
But since WPF can´t inherit a control with xaml, I can´t get my head around a solution.
Let´s say I have this control.
<StackPanel x:Class="Framework.UI.Controls.Base.Navigator.NavigatorItem"
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:Framework.UI.Controls.Base.Navigator"
mc:Ignorable="d"
d:DesignHeight="26" d:DesignWidth="200">
<Button Name="btnHeader" Content="Button"/>
<TreeView Name="tvNavContent" Height="0"/>
</StackPanel>
In codebehind the Button is being used for a Click event as well as the header Text, which I want to be filled from the Control that inherits from this.
And with a function the TreeView "tvNavContent" is being filled with something like this:
<TreeViewItem x:Class="Framework.UI.Controls.Base.Navigator.NavigatorEntry"
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:Framework.UI.Controls.Base.Navigator"
mc:Ignorable="d"
d:DesignHeight="20" d:DesignWidth="200">
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<Image Name="imgIcon" Width="16" Height="16" Stretch="Fill"/>
<TextBlock Name="txtTitle"/>
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
What I want to achieve is to reuse the Stackpanel with the Button and TreeView inside and with it´s functions.
I tried two things:
First I tried to create a template and applied that to the base class. After that I just tried to load the controls of the template in the base class with the FindName<>() function.
The problem here is, that the template is applied after InitializeComponent().
But during InitializeComponent() I already need access, to set the controls header property for the title from the control that inherits from the base class.
After that I tried to implement the child controls completely in the base class of the control.
Just created them in the constructor and added them to the Children Property of the stackpanel the base class inherits from.
That did (somewhat) work.
But apparently the controls behave completely different when created like that.
No matter the settings. I just couldn´t get the controls to fit correctly inside their parents.
Furthermore, this method is completely unsuitable for a larger project, when it comes to theme adjustments.
Can someone guide me in the correct direction here?
Create a class called NavigatorItem (without any .xaml file):
public class NavigatorItem : Control
{
static NavigatorItem()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(NavigatorItem),
new FrameworkPropertyMetadata(typeof(NavigatorItem)));
}
}
Create a ResourceDictionary called generic.xaml and put it in a folder called themes (these names are by convention) at the root of your project, and define a default template for the NavigatorItem class in there:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp12">
<Style TargetType="local:NavigatorItem">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:NavigatorItem">
<StackPanel>
<Button Name="btnHeader" Content="Button"/>
<TreeView Name="tvNavContent" Height="0"/>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
You can then override the OnApplyTemplate of the NavigatorItem class to get a reference to the elements in the template and hook up event handlers to them, e.g.:
public override void OnApplyTemplate()
{
Button button = GetTemplateChild("btnHeader") as Button;
button.Click += Button_Click;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("button clicked!");
}

MouseLeftButtonUp does not fire for ScrollViewer in WPF control template

In WPF and C# I am trying to set up a mouse drag scrolling feature for a ScrollViewer contained within a control template for a document viewer. The problem: I have not been able to get the MouseLeftButtonEvent to fire.
It is basically the default DocumentViewer template, with a few features modified. Here is an outline in XAML:
<Style x:Key="DocumentViewerStyle1" BasedOn="{x:Null}" TargetType="{x:Type DocumentViewer}">
<!--...—>
<Setter Property="ContextMenu" Value="{x:Null}" /> <!--So does not mess up right click, if I use that-->
<!--...-->
<ControlTemplate TargetType="{x:Type DocumentViewer}">
<!--...-->
<ScrollViewer x:Name="PART_ContentHost" CanContentScroll="True"
IsHitTestVisible="True" HorizontalScrollBarVisibility="Auto"
Grid.Row="1" Loaded ="OnScrollViewerLoaded" />
<DockPanel Grid.Row="1" >
<!-...-->
</ControlTemplate>
</Style>
I use the following in code behind so that I can access the ScrollViewer. If one changes “Left” to “Right” in the method below, it works to perfection, but of course with the right mouse button rather than the left.
public partial class PrintPreview : Window
{
private ScrollViewer nomad;
etc. and
private void OnScrollViewerLoaded(object sender, RoutedEventArgs e)
{
nomad = (ScrollViewer)sender;
nomad.AddHandler(MouseLeftButtonDownEvent, new MouseButtonEventHandler(OnMouseButtonDown), true);
nomad.AddHandler(MouseLeftButtonUpEvent, new MouseButtonEventHandler(OnMouseButtonUp), true);
nomad.AddHandler(MouseMoveEvent, new MouseEventHandler(OnMouseMove), true);
}
The OnMouseButtonUp event handler, for example, is
private void OnMouseButtonUp(object sender, MouseButtonEventArgs e)
{
nomad.Cursor = Cursors.IBeam;
nomad.ReleaseMouseCapture();
}
Have tried various things found here: No help from using Preview events for my three mouse events. No help from setting Focusable="False" for the ScrollViewer, or for setting a Background for it. Any suggestions? Thanks!

How can I access a named element of a ControlTemplate through code-behind?

I want to access one of the named elements within the original control template that another element is using, in the code-behind.
This is an example of the XAML code (obviously the original is more complicated, or I'd just be doing this in XAML):
<Window x:Class="Temp.MainWindow" Title="MainWindow">
<Window.Resources>
<ControlTemplate x:Key="MyTemplate" TargetType="{x:Type Expander}">
<Expander Header="Some header">
<StackPanel>
<Grid Name="MyGrid"/>
</StackPanel>
</Expander>
</ControlTemplate>
</Window.Resources>
<Grid>
<Expander Name="expander" Template="{DynamicResource MyTemplate}"/>
</Grid>
</Window>
What I've tried:
public MainWindow()
{
InitializeComponent();
Grid grid = expander.Template.FindName("MyGrid", expander) as Grid;
}
I've also tried
Grid grid = expander.Template.Resources.FindName("MyGrid") as Grid;
But g is always null.
I've looked at:
How do I access an element of a control template from within code behind?
How to access a WPF control located in a ControlTemplate
How do I programmatically interact with template-generated elements Part I
The links above are how I got the code I'm working with, but for some reason, g is just always null. Am I doing something wrong with the ContentTemplate? Any help would be appreciated!
You need to wait until the template is applied to the control
protected override OnApplyTemplate()
{
Grid grid = Template.FindName("YourTemplateName") as Grid;
}
The real problem here is that you're mixing technologies. You're attempting to use something meant for grabbing the template of a lookless control, in the behind code of the main window. I would be surprised if you didn't run into more issues.
Instead, I would suggest looking into How to Create Lookless Controls and redesigning your application. It wouldn't take much effort and it would all play nice together.

Items of ItemsPresenter in Custom-Control not shown correctly

I've written a custom control that should display items in a list and provide additional commands related to scrolling events, like load more. So I decided to create a ScrollViewer and add the ItemsPresenter inside generic.xaml.
Basically this works fine when I use ItemsControl as base class. But now I need the possibility to click on a single item. The solution is to use the ListView class as base class.
Here comes the problem. As soon as I use a GridView or ListView as base class the content of the list is only shown as far as it is shown at inital offset of the list. other/new items that you can only see by scrolling down aren't shown.
I thought that the list maybe dont resize, but adding a footer and a border around the list shows the the list resizes correctly.
The collection I use is a ObservableCollection and works with the ItemsControl base class.
I think the problem is a setting or something on xaml side. But I dont know where and i have no ideas what to search next. everytime I search, all results are marked as clicked =(
Here is my xaml code of the generic.xaml:
<Style TargetType="lists:ListViewWithCommands">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="lists:ListViewWithCommands">
<Border
Background="{TemplateBinding Background}"
BorderBrush="Red"
BorderThickness="1">
<ScrollViewer x:Name="ItemScrollViewer">
<Border BorderBrush="White" BorderThickness="2">
<StackPanel>
<ItemsPresenter />
<ContentPresenter Visibility="{TemplateBinding LoadingTemplateVisibility}" ContentTemplate="{TemplateBinding LoadingTemplate}" />
</StackPanel>
</Border>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Here is a part of my control itselve:
[TemplatePart(Name="ItemScrollViewer", Type=typeof(ScrollViewer))]
public sealed class ListViewWithCommands : ListView
{
private ScrollViewer _itemScrollViewer;
public ListViewWithCommands()
{
this.DefaultStyleKey = typeof(ListViewWithCommands);
}
protected override void OnApplyTemplate()
{
base.OnApplyTemplate();
this._itemScrollViewer = GetTemplateChild("ItemScrollViewer") as ScrollViewer;
if (this._itemScrollViewer != null)
{
Debug.WriteLine(String.Format("ItemScrollViewer found! Attatching Event Handler!"), this.GetType().Name);
this._itemScrollViewer.ViewChanged += _itemScrollViewer_ViewChanged;
}
else
{
throw new NullReferenceException("ItemScrollViewer not found!");
}
}
....
I hope you have any suggestions.
robert

WPF custom Textbox control isn't properly binding text

I have a custom TextBox control. I am trying to bind it to a simple Description string property of an object. How can I get the binding to work? The same binding works fine if I change this to a TextBox.
<Style x:Key="{x:Type TaskDash:TextBoxWithDescription}" TargetType="{x:Type TaskDash:TextBoxWithDescription}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TaskDash:TextBoxWithDescription}">
<TextBox>
</TextBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
public class TextBoxWithDescription : TextBox
{
public TextBoxWithDescription()
{
LabelText = String.Empty;
}
public string LabelText { get; set; }
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
var textBlock = (TextBlock)this.Template.FindName("LabelText", this);
if (textBlock != null) textBlock.Text = this.LabelText;
}
}
<TaskDash:TextBoxWithDescription Grid.Row="0" Grid.Column="0"
x:Name="textBoxDescription"
Text="{Binding Description, BindsDirectlyToSource=True}" LabelText="Description">
</TaskDash:TextBoxWithDescription>
public partial class EditTaskItem : Window
{
private TaskItem _taskItem;
public EditTaskItem(TaskItem taskItem)
{
InitializeComponent();
this.DataContext = taskItem;
textBoxDescription.DataContext = taskItem;
_taskItem = taskItem;
}
}
Ok ... there are a couple of things wrong with your code.
Lets begin with your style for your custom control. You need to add a static constructor which allows restyling your new control. Like this
static TextBoxWithDescription ()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBoxWithDescription ), new FrameworkPropertyMetadata(typeof(TextBoxWithDescription )));
}
This tells WPF "Hey, please look for a Style for this control".
Now you can remove the x:Key="{x:Type TaskDash:TextBoxWithDescription}"because this is going to be your default style.
Next thing is. In WPF one thing to understand is, that every control has absolutely no UI content, unless it gets an Template. You already have a Template in your Style, but the only visual content it gets is an empty TextBox. This is strange, because you derive your TextBoxWithDescription from TextBox. So what you create is a a control derived from textbox, containing a textbox.
See this to see how a TextBox Template looks in WPF 4.0.
Back to your empty TextBox in your ControlTemplate. Remember that i said, your controls, without a style are completely invisible, its only logic. The only visible thing is the TextBox in your Template. To make it work somehow, you need to pass some properties to this TextBox. The control says how and the template takes the properties and puts them in use.
You can do this via TemplateBinding
For example. If your control has a Property Background, this property does nothing you can set it as long as you want, but a ControlTemplate can give some meaning to it. So for example add a Rectangle in your ControlTemplate and set the Fill Property to {TemplateBinding Background}. Which basicaly tells the Rectangle "Your property Fill is going to be the value of Background from the control we are currently templating". I hope this makes it clear.
Next thing: You overrid OnApplyTemplate you usually do this to find a control in your ControlTemplate by name. It seems you mixed this with one of your properties. To Find a control in your Template via FindName, you need to give it a name
Like this
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TaskDash:TextBoxWithDescription}">
<TextBox x:Name="PART_MyTextBox">
</TextBox>
</ControlTemplate>
</Setter.Value>
</Setter>
and modify your OnApplyTemplate to
var textBlock = (TextBlock)this.Template.FindName("PART_MyTextBox", this);
Now you have the textblock in your current ControlTemplate.
The last mistake i can see is.
You set in OnApplyTemplatethe TextBox Text to your LabelText. While this works, one time, it is not very nice and usually not the WPF way. Also if you modify the LabelText property, it will not be displayed, because you would have to set it again.
Change your LabelText to a dependency property Now you can use the already mentioned TemplateBinding to set this text, directly on your control template TextBox.
<TextBox x:Name="PART_MyTextBox" Text="{TemplateBinding LabelText}>
</TextBox>
This will also make sure that changes to your Control property, will also update the Text on this TextBox.
One last thing
this.DataContext = taskItem;
// textBoxDescription.DataContext = taskItem;
_taskItem = taskItem;
If your textboxdescription is going to be a parent of your window, you don't need to set the DataContext explicitly, because it will be inherited down the hierachy. As long as an Element don't change the DataContext, it will be always the DataContext of the parent.
I would suggest you read more on the basics of WPF, i know it has a steep learning curve, but its worth the effort. It is difficult if someone comes from a WinForms background to get the head wrapped around all the different new design philosophies and different ways to do things.
Hope that helps a bit
#dowhilefor - great answer. I write this as an answer only because it's going to be too long for a comment.
#Shawn - it seems like you are trying to make a Label-Field control. If that's the case, try this template:
<ControlTemplate TargetType="{x:Type TaskDash:TextBoxWithDescription}">
<Grid>
<Grid.ColumnDefinitions>
<!--The SharedSizeGroup property will allow us to align all text boxes to the same vertical line-->
<ColumnDefinition Width="Auto"
SharedSizeGroup="LabelColumn"/>
<!--This column acts as a margin between the label and the text box-->
<ColumnDefinition Width="5"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="{TemplateBinding LabelText}"
VerticalAlignment="Center"/>
<Border Grid.Column="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer x:Name="PART_ContentHost"
Padding="{TemplateBinding Padding}"/>
</Border>
</Grid>
</ControlTemplate>
And remove the override for OnApplyTemplate.
If the control is a part of a (often called) "PropertyGrid" and you have multiple instances of the TextBoxWithDescription control in the same panel, define Grid.IsSharedSizeScope on that panel (it doesn't have to be Grid panel). This will align the text boxes to a uniform vertical line.

Categories

Resources