WPF Custom Control - How to properly loop through and display child content - c#

Attempting to write a custom control which permits a collection of children content. The end goal is to have some functional filtering of the child elements based on custom attached dependency properties; however, before getting that far, any attempts I've made to collect and re-display child UIElements have ended in exceptions during XAML parsing/display.
I have a class CustomFilter : Control which has an associated default style in a nearby xaml file. It has a dependency property for Children, which currently collect the child elements in a UIElementCollection.
I believe my problem may be how I am attempting to render the collection of children. The only way I know of looping through content in XAML involves using an ItemsControl, to which I am passing my Children collection as the ItemsSource, which feels rather backwards (using UIElements as data models?)
Afterwards, we attempt to render the elements via a content presenter inside of the ItemTemplate. I've seen in many other examples which render control children using content presenters, so I expect this is probably at least partially correct (though all only using single elements).
The sample class
[ContentProperty(nameof(Children))]
public class CustomFilter : Control
{
static CustomFilter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomFilter), new FrameworkPropertyMetadata(typeof(CustomFilter)));
}
public CustomFilter()
{
Children = new UIElementCollection(this, this);
}
public static readonly DependencyProperty ChildrenProperty = DependencyProperty.Register(nameof(Children), typeof(UIElementCollection), typeof(CustomFilter), new FrameworkPropertyMetadata(RefreshFilter));
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public UIElementCollection Children
{
get { return (UIElementCollection)GetValue(ChildrenProperty); }
private set { SetValue(ChildrenProperty, value); }
}
}
The sample template
<Style TargetType="{x:Type local:CustomFilter}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomFilter}">
<StackPanel>
<ItemsControl ItemsSource="{TemplateBinding Children}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<ContentPresenter Content="{Binding}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The use case
<local:CustomFilter>
<TextBlock Text="X"/>
<TextBlock Text="Y"/>
<TextBlock Text="Z"/>
</local:CustomFilter>
Attempting to run use this control whatsoever causes it to fail when it reaches the ItemsControl, without reaching the ItemTemplate, with the following exception:
ArgumentException: Specified Visual is already a child of another Visual or the root of a CompositionTarget.
I've attempted to create a wrapper class for the Children, passing one child to each instance of the wrapper, and binding to that as an ItemsSource - it successfully loops through the ItemTemplate, but attempting to use the ContentPresenter there on the wrapped child element provides me with the following exception instead:
ArgumentException: Must disconnect specified child from current parent Visual before attaching to new parent Visual.

Why are you using an UIElementCollection instead of a List<UIElement>? You are not supposed to bind to an UIElementCollection in a template.
Your example should work if you define a List<UIElement> dependency property:
[ContentProperty(nameof(Children))]
public class CustomFilter : Control
{
static CustomFilter()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomFilter), new FrameworkPropertyMetadata(typeof(CustomFilter)));
}
public CustomFilter()
{
SetValue(ChildrenPropertyKey, new List<UIElement>());
}
private static readonly DependencyPropertyKey ChildrenPropertyKey =
DependencyProperty.RegisterReadOnly(
nameof(Children),
typeof(List<UIElement>),
typeof(CustomFilter),
new FrameworkPropertyMetadata(new List<UIElement>())
);
public static readonly DependencyProperty ChildrenProperty =
ChildrenPropertyKey.DependencyProperty;
public List<UIElement> Children
{
get { return (List<UIElement>)GetValue(ChildrenProperty); }
}
}

Related

Proper way to create Custom Bindable WPF Control

I want to ask about the right way if I want to create Bindable user control consisting of two controls. I am not sure about what I am doing - whether I do it correctly , because I run into some problems.
Here is what I am trying to do:
Lets call this control ucFlagControl . Create new , custom user control ...
Its purpose is to show Color interpretation of logic ( True/ False ) value in variable , type of Bool.
What I used to do before was that I use Rectangle, and Bind FillProperty to boolean value using Converter
What I did to make it works was , that I made a usercontrol , and put rectangle and label inside
than I added this code:
public partial class ucStatusFlag : UserControl
{
public ucStatusFlag()
{
InitializeComponent();
}
public string LabelContent
{
get { return (string)GetValue(LabelContentProperty); }
set
{
SetValue(LabelContentProperty, value);
OnPropertyChanged("LabelContent");
}
}
///in case that I use integer or array
public int BitIndex
{
get { return (int)GetValue(BitIndexProperty); }
set
{
SetValue(BitIndexProperty, value);
OnPropertyChanged("BitIndex");
}
}
public string BindingSource
{
get { return (string)GetValue(BindingSourceProperty); }
set
{
SetValue(BindingSourceProperty, value);
OnPropertyChanged("BindingSource");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(name));
}
}
/// <summary>
/// Identified the Label dependency property
/// </summary>
public static readonly DependencyProperty LabelContentProperty =
DependencyProperty.Register("LabelContent", typeof(string), typeof(ucStatusFlag), new PropertyMetadata("LabelContent"));
public static readonly DependencyProperty BitIndexProperty =
DependencyProperty.Register("BitIndex", typeof(int), typeof(ucStatusFlag), new PropertyMetadata(0));
public static readonly DependencyProperty BindingSourceProperty =
DependencyProperty.Register("(BindingSource", typeof(string), typeof(ucStatusFlag), new PropertyMetadata(""));
private void StatusFlag_Loaded(object sender, RoutedEventArgs e)
{
if (BindingSource.Length > 0)
{
Binding bind = new Binding();
string s = LabelContent;
int i = BitIndex;
bind.Converter = new StatusToColor();
bind.Path = new PropertyPath(BindingSource);
bind.ConverterParameter = BitIndex.ToString();
bind.Mode = BindingMode.OneWay;
bind.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
recStatusBit.SetBinding(Rectangle.FillProperty, bind);
}
}
private class StatusToColor : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
byte bDataWordIdx;
byte bDataBitIdx;
Byte.TryParse((string)parameter, out bDataBitIdx);
if (Object.ReferenceEquals(typeof(UInt16[]), value.GetType()))
{
UInt16[] uiaData = (UInt16[])value;
bDataWordIdx = (byte)uiaData[0];
if ((uiaData[bDataBitIdx / 16] >> (bDataBitIdx % 16) & 0x1) == 1)
{
return Brushes.Green;
}
else
{
return Brushes.Red;
}
}
else if (Object.ReferenceEquals(typeof(UInt16), value.GetType()))
{
UInt16 uiaData = (UInt16)value;
if (((uiaData >> bDataBitIdx) & 0x1) == 1)
{
return Brushes.Green;
}
else
{
return Brushes.Red;
}
}
return 0;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return 0;
}
}
}
}
Than I realized that I can easily bind content and I do not have to create public static readonly DependencyProperty LabelContentProperty
but just property
public new string Content
{
get { return (string)label.Content; }
set
{
SetValue(label.Content, value);
OnPropertyChanged("Content");
}
}
this overrides the original content so I am able to Bind and/or assign the text of the label in upper level - in e.g. MainWindow.xaml where this user control is put
First question is if this is in this case OK or if there is some background I am not aware of and I should even such small controls do in different way - I would like to make dll. from it an load it to toolbox - I tested it works. And than use it in for example stack panel .
Second question is that I have problem with a rectangle "Fill" property . I am not able to bind that property like I bind content .
I know that the rectangle is derived from Shape class so I am not sure if it has something to do with this.
If I am able to do the inner binding or connection same as in
Content
I can remove the converters than and just bind it in e.g. MainWindow.xaml file (using the converter and converter parameter )
But FillProperty does not work for me so I am not sure about my point of view .
Thank you for suggestions
EDIT:
well I am sorry but I did not catch all you want to say in a comment below. Could you please explain closer ?
I know that the code above is not the right way to do it ... ?
Or can you post any article about it ?
my actual code is like this:
In a user control ... I removed all the code from code behind ...
' <Label x:Name="lStatusBit" Grid.Column="1" Padding="0" VerticalContentAlignment="Center" Margin="2,1,17,2" />
<Rectangle x:Name="recStatusBit" Margin="0,3,1,7" />'
Content property works, I cant see Rectangle , and rectangle fill property ...
Other problem is if I fill in Content property in XAML where my uc is placed , Rectangle disappears .
I know I'm a year late to the party, but I'll answer incase anyone else comes across this.
My Suggestions
You should use a TextBlock control instead of Label controls if you want to display pure text. Labels have a content element which is re-rendered/computed many more times than a TextBlock's simple Text property.
You should avoid using magic strings, e.g. "LabelContent". You should use the C# nameof() expression when referencing property names. For example:
I use lambda expressions to clean up the code a bit, but this is just preference.
public string LabelContent
{
get => (string)GetValue(LabelContentProperty);
set => SetValue(LabelContentProperty, value);
}
public static readonly DependencyProperty LabelContentProperty =
DependencyProperty.Register(
nameof(LabelContent),
typeof(string),
typeof(ucStatusFlag),
new PropertyMetadata("Default Value"));
This will prevent runtime errors due to mistyped text, will allow you to jump to the property's reference, will make refactoring easier, and will make debugging easier by giving you a compile error that's easy to find (if the property doesn't exist).
I don't think you need the rectangle. If you're just trying to change the background color of the text area you can use a DataTrigger or make a converter.
DataTrigger Example
<TextBlock>
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<!-- The default value -->
<Setter Property="Background" Value="Transparent" />
<!-- Your trigger -->
<Style.Triggers>
<DataTrigger Binding="{Binding SomeBooleanValue}" Value="True">
<Setter Property="Background" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
A DataTrigger is a quick and easy way to style a control by binding to a property on your ViewModel (assuming you're using the MVVM structure), but there are some cons - like reusing the same style on a different View whose ViewModel's properties are different. You'd have to rewrite the entire styling again.
Lets turn it into a reusable control where we can (1) specify a highlight background color, and (2) use a boolean to determine whether the control is highlighted.
Template Controls vs UserControls
I make my templated controls in a separate C# class file and put the control's styling in another separate resource dictionary file instead of using a UserControl.
These templated controls can consist of several other controls to make a single reusable control.
It's my understanding that UserControls are meant to use multiple templated controls (e.g. a TextBox) and link their interactions together to perform a specific way.
I don't think these controls are meant to be reusable in separate unrelated projects - they display data depending on your ViewModel which can be situational.
If you want to extend your custom control in the future via inheritance, then using a UserControl will make things difficult.
Here's what a few of my controls look like in the solution explorer:
Solution Files Snippet
The ExpansionPanel control in the snippet is an Expander with additional functionalities/properties.
The NavButton is a Button with additional functionalities/properties also.
I have a NavigationView UserControl that uses both of those controls to create something much larger than a templated control.
It sounds like you want to create a reusable templated control.
Creating a Custom Control
Here are the basic steps:
Create a "Themes" folder at the root of your project. It must be at the root of your project and spelling does matters.
Create a Generic.xaml Resource Dictionary file in the "Themes" folder. It must be directly under the "Themes" folder and spelling does matters.
This is where you store the default themes for your custom controls.
The template style for your control will automatically be added to the Generic.xaml file when you add a Custom Control template to your project.
<Style TargetType="{x:Type local:Example}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Example}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Personally, I like to have separate .xaml file for each control, and then I merge it into the Generic.xaml resource dictionary. This is just for organization purposes.
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<!-- Control template styles -->
<ResourceDictionary Source="pack://application:,,,/Themes/ExpansionPanel.xaml" />
<ResourceDictionary Source="pack://application:,,,/Themes/NavButton.xaml" />
<ResourceDictionary Source="pack://application:,,,/Themes/TextDocument.xaml" />
<ResourceDictionary Source="pack://application:,,,/Themes/TextDocumentToolBar.xaml" />
<ResourceDictionary Source="pack://application:,,,/Themes/TextEditor.xaml" />
<ResourceDictionary Source="pack://application:,,,/Themes/HighlightTextBlock.xaml" />
<!-- etc... -->
</ResourceDictionary.MergedDictionaries>
<!-- Other styles or whatever -->
</ResourceDictionary>
It's important to note that order does matter if you have controls that depend on other controls.
Merge the Generic.xaml file into your App.xaml file.
<Application>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- Other resource dictionaries... -->
<ResourceDictionary Source="pack://application:,,,/Themes/Generic.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Other resource dictionaries... -->
</ResourceDictionary>
</Application.Resources>
</Application>
Why not just merge the control templates in the App.xaml file directly? WPF looks directly for the Generic.xaml file for custom type themes. App.xaml is also application specific and wouldn't be able to be usable in other applications if you used the library as a control library.
Create a .cs file using the built in Custom Control template OR a standard C# class file.
Your control's .cs file would resemble something similar to...
public class HighlightTextBlock : Control
{
#region Private Properties
// The default brush color to resort back to
public Brush DefaultBackground;
#endregion
static HighlightTextBlock()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(HighlightTextBlock), new FrameworkPropertyMetadata(typeof(HighlightTextBlock)));
}
// Get the default background color and set it.
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
DefaultBackground = Background;
}
#region Dependency Properties
/// <summary>
/// The text to display.
/// </summary>
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
nameof(Text), typeof(string), typeof(HighlightTextBlock), new PropertyMetadata(string.Empty));
public string Text
{
get => (string)GetValue(TextProperty);
set => SetValue(TextProperty, value);
}
/// <summary>
/// Whether or not the background should be highlighted.
/// </summary>
// This uses a callback to update the background color whenever the value changes
public static readonly DependencyProperty HighlightProperty = DependencyProperty.Register(
nameof(Highlight), typeof(bool),
typeof(HighlightTextBlock), new PropertyMetadata(false, HighlightPropertyChangedCallback));
public bool Highlight
{
get => (bool)GetValue(HighlightProperty);
set => SetValue(HighlightProperty, value);
}
/// <summary>
/// The highlight background color when <see cref="Highlight"/> is true.
/// </summary>
public static readonly DependencyProperty HighlightColorProperty = DependencyProperty.Register(
nameof(HighlightColor), typeof(Brush),
typeof(HighlightTextBlock), new PropertyMetadata(null));
public Brush HighlightColor
{
get => (Brush)GetValue(HighlightColorProperty);
set => SetValue(HighlightColorProperty, value);
}
#endregion
#region Callbacks
// This is the callback that will update the background
private static void HighlightPropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var target = (HighlightTextBlock)dependencyObject;
if (target.Highlight)
target.Background = target.HighlightColor;
else
target.Background = target.DefaultBackground;
}
#endregion
}
Create a ResourceDictionary.xaml file to store your control's template and style OR add it directly in Generic.xaml.
Your .xaml file would look something like...
<Style x:Key="HighlightTextBlock" TargetType="{x:Type ctrl:HighlightTextBlock}">
<!-- Default setters... -->
<!-- Define your control's design template -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ctrl:HighlightTextBlock}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<!--
I only bound the Text and Background property in this example
Make sure to bind other properties too.. like Visibility, IsEnabled, etc..
-->
<TextBlock Text="{TemplateBinding Text}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
Set the default style for the control
The above style has a key, so controls won't use that style
unless the style is explicitly set.
e.g.
<ctrl:HighlightTextBlock Style={StaticResource HighlightTextBlock} />
The reason I used a key above is to allow extending/reusing that default style.
If a key wasn't present then you wouldn't be able to reference it in
another style.
-->
<Style TargetType="{x:Type ctrl:HighlightTextBlock}" BasedOn="{StaticResource HighlightTextBlock}" />
Add a reference to the control's resource dictionary in Generic.xaml, like in step 2's code snippet.
Usage:
I'm binding the IsChecked property to a IsHighlighted property on my ViewModel.
You can bind it to whatever.
<StackPanel>
<ToggleButton IsChecked="{Binding IsHighlighted}" Content="{Binding IsHighlighted}"
Width="100" Height="35" Margin="5"/>
<ctrl:HighlightTextBlock Background="Transparent" HighlightColor="Red"
Text="HELLO WORLD!!!" Highlight="{Binding IsHighlighted}"
Width="100" Height="35" HorizontalAlignment="Center" />
</StackPanel>
On False Snippet
On True Snippet
Your controls may look a bit different - I'm using a custom dark theme.
Finally I found this working :
XAML
<UserControl x:Class="ucStatusFlag"
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="17" d:DesignWidth="100"
x:Name="StatusFlag">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Rectangle x:Name="recStatusBit" Grid.Column="0" Stroke="Black" Width="11" Fill="{Binding ElementName=StatusFlag, Path=RectangleColor}" Margin="0,2,0.2,3.8" />
<Label Height="17" x:Name="lStatusBit" Foreground="Black" Grid.Column="1" Padding="0" VerticalContentAlignment="Center" Margin="5,0,0,1" Content="{Binding ElementName=StatusFlag, Path=LabelContent}" />
</Grid>
C#
public partial class ucStatusFlag : UserControl
{
public ucStatusFlag()
{
InitializeComponent();
}
public string LabelContent
{
get { return (string)GetValue(LabelContentProperty); }
set
{
SetValue(LabelContentProperty, value);
}
}
public SolidColorBrush RectangleColor
{
get { return (SolidColorBrush)GetValue(RectangleColorProperty); }
set
{
SetValue(RectangleColorProperty, value);
}
}
public static readonly DependencyProperty RectangleColorProperty =
DependencyProperty.Register("RectangleColor", typeof(SolidColorBrush), typeof(ucStatusFlag), new PropertyMetadata(Brushes.Gold));
public static readonly DependencyProperty LabelContentProperty =
DependencyProperty.Register("LabelContent", typeof(string), typeof(ucStatusFlag), new PropertyMetadata("LabelContent"));
}
Binding in Another Project :
<ucStatusFlag HorizontalAlignment="Left" Height="18" Margin="154,224,0,0" VerticalAlignment="Top" Width="100" LabelContent="ABC" RectangleColor="{Binding RectangleColorPropertyInProject}"/>
Where RectangleColorPropertyInProject is Property In certain project view model

Dependency Property does not work after view model was initialized

I would like to write a custom asynchronous image container custom control. I have created a list from this control:
<ItemsControl ItemsSource="{Binding Items}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<custom:CustomImage Width="64" Height="64" BaseUri="{Binding Uri}" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The Items property is a list of object A what I initialize in the MainWindowViewModel:
public List<A> Items { get; set; } = new List<A>();
and
foreach (XmlNode item in doc.LastChild.FirstChild.SelectNodes(".//item"))
{
Items.Add(
new A
{
Title = item.FirstChild.InnerText,
Uri = new Uri(item.SelectNodes(".//enclosure")[0].Attributes["url"].Value)
}
);
}
I want to set a Dependency Property on the custom control (you can see above: BaseUri="{Binding Uri}". Uri is a property of the class A.
This is the DP implementation:
public Uri BaseUri
{
get { return (Uri)GetValue(BaseUriProperty); }
set { SetValue(BaseUriProperty, value); }
}
public static readonly DependencyProperty BaseUriProperty =
DependencyProperty.Register("BaseUri", typeof(Uri),
typeof(CustomImage), new FrameworkPropertyMetadata(default(Uri), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
It works only if the CustomImage custom control doesn't have any view model. If I do this in the CustomImage's constructor:
DataContext = new CustomImageViewModel();
it doesn't work anymore.
Any idea?
You should never explicitly set the DataContext of a UserControl. Doing so effectively prevents that the DataContext is inherited from the control's parent, as it is required by a binding like
BaseUri="{Binding Uri}"
So, remove the line
DataContext = new CustomImageViewModel();
from the constructor of your control.
It is not true that the "control doesn't have any view model" when you don't set it explicitly. In fact, the view model (or the inherited DataContext) is set - via the ItemsControl's item container - to the appropriate item from the ItemsSource collection. So the DataContext of your control is automatically set to an instance of your class A.

TabControl inside CustomControl loads with no selected tab item

I wrote a CustomControl that supports direct content. When I nest a TabControl in it, there's no tab item selected on launch. It happens only when using ItemsSource. Anyone know what's wrong?
Sample implementation
In MainWindow.xaml:
<Window.DataContext>
<local:VM/>
</Window.DataContext>
<Grid Margin="20">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TabControl ItemsSource="{Binding Data}"/>
<local:CustomControl1 Grid.Column="1">
<TabControl ItemsSource="{Binding Data}" />
</local:CustomControl1>
</Grid>
VM.cs
class VM
{
public List<int> Data { get; set; } = new List<int>{ 1, 2, 3, 4 };
}
CustomControl1:
[ContentProperty("Content")]
public class CustomControl1 : Control
{
static CustomControl1()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
}
public FrameworkElement Content
{
get { return (FrameworkElement)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register("Content", typeof(FrameworkElement), typeof(CustomControl1), new PropertyMetadata(null));
}
In Generic.xaml:
<Style TargetType="{x:Type local:CustomControl1}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:CustomControl1}">
<ContentPresenter/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
After some time of poking around I've found a solution, but I am not sure of the exact reasons behind this behavior.
I've modified your example and compared the behavior when the TabControl was put inside a ContentControl and a CustomControl1. Predictably, the first tab in case of ContentControl was selected, but in case of CustomControl1 it was not. After inspecting the ContentControl source code (in particular the ContentControl.OnContentChanged method) we can see that what it does is it sets its content as its logical child.
I then confirmed that setting the TabControl as the CustomControl1's logical child does the trick (but I'm not sure why, as I mentioned). So the minimal solution to your problem is to handle the logical relation between your control and its content in the property changed callback, e.g.:
public static readonly DependencyProperty ContentProperty =
DependencyProperty.Register(
"Content",
typeof(FrameworkElement),
typeof(CustomControl1),
new PropertyMetadata(null)
{
PropertyChangedCallback = OnContentChanged
});
private static void OnContentChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = (CustomControl1)d;
if (e.OldValue != null)
control.RemoveLogicalChild(e.OldValue);
if (e.NewValue != null)
control.AddLogicalChild(e.NewValue);
}
Note though that this lacks some checks (e.g. if new value already does have a logical parent or the control is a part of a template), so you might want to simply copy the code from the referenced source, or fallback to deriving your control from ContentControl altogether.

WPF: initial visibility and template loading

I have encountered an issue where the bindings are not properly set on one of my user controls if that control is not visible when it is initialized. I have duplicated the issue with the following dumbed-down controls:
public class Test3 : Control
{
static Test3()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Test3), new FrameworkPropertyMetadata(typeof(Test3)));
}
public string Test
{
get { return (string)GetValue(TestProperty); }
set { SetValue(TestProperty, value); }
}
public static readonly DependencyProperty TestProperty =
DependencyProperty.Register("Test", typeof(string),
typeof(Test3), new UIPropertyMetadata("test3 default text"));
}
public class Test2 : Control
{
static Test2()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Test2), new FrameworkPropertyMetadata(typeof(Test2)));
}
public FrameworkElement Test3Control
{
get { return (FrameworkElement)GetValue(Test3ControlProperty); }
set { SetValue(Test3ControlProperty, value); }
}
public static readonly DependencyProperty Test3ControlProperty =
DependencyProperty.Register("Test3Control", typeof(FrameworkElement),
typeof(Test2), new UIPropertyMetadata(null));
public string Test
{
get { return (string)GetValue(TestProperty); }
set { SetValue(TestProperty, value); }
}
public static readonly DependencyProperty TestProperty =
DependencyProperty.Register("Test", typeof(string), typeof(Test2),
new UIPropertyMetadata("test2 default text"));
}
public partial class Test1 : UserControl
{
public Test1()
{
InitializeComponent();
}
public string Test
{
get { return (string)GetValue(TestProperty); }
set { SetValue(TestProperty, value); }
}
public static readonly DependencyProperty TestProperty =
DependencyProperty.Register("Test", typeof(string),
typeof(Test1), new UIPropertyMetadata("test1 default text"));
}
XAML for usercontrol:
<UserControl x:Class="Test.Test1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:dummy="clr-namespace:WpfTestApplication.Test"
Name="ucThis">
<UserControl.Resources>
<Style TargetType="{x:Type test:Test2}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type test:Test2}">
<GroupBox Header="Test2">
<StackPanel>
<TextBox IsEnabled="False" Text="{TemplateBinding Test}"/>
<GroupBox Header="Test3">
<ContentPresenter Content="{TemplateBinding Test3Control}"/>
</GroupBox>
</StackPanel>
</GroupBox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
<Grid>
<test:Test2 Test="{Binding ElementName=ucThis,Path=Test}">
<test:Test2.Test3Control>
<test:Test3 Test="{Binding ElementName=ucThis,Path=Test}">
<test:Test3.Template>
<ControlTemplate TargetType="{x:Type test:Test3}">
<TextBox IsEnabled="False" Text="{TemplateBinding Test}"/>
</ControlTemplate>
</test:Test3.Template>
</test:Test3>
</test:Test2.Test3Control>
</test:Test2>
</Grid>
</UserControl>
... and XAML for main window (the guts of it, anyway):
<DockPanel>
<StackPanel>
<TextBox Name="tbInput"/>
<Expander Header="Initially Visible" IsExpanded="True">
<test:Test1 Test="{Binding ElementName=tbInput, Path=Text}" />
</Expander>
<Expander Header="Initially Collapsed" IsExpanded="False">
<test:Test1 Test="{Binding ElementName=tbInput, Path=Text}" />
</Expander>
</StackPanel>
</DockPanel>
I would expect that whatever text is entered into the textbox ("tbInput") would be displayed in the Test2 and Test3 boxes - and indeed it is for both instances of Test2 but only for the Test3 that is initially visible. The Test3 that is initially collapsed always displays the default text, even if it is visible when the text is entered.
I've tried to investigate this using Snoop, but the issue corrects itself when I evaluate the relevant parts of the tree w/ Snoop so it hasn't been much help.
What is causing this behavior? How can I correct it? Where can I read more about it?
UPDATE:
By watching the output window I discovered this error message:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'ElementName=ucThis'. BindingExpression:Path=Test; DataItem=null; target element is 'Dummy3' (Name=''); target property is 'Test' (type 'String')
By handling the loaded and initialized events on these controls I can see that this error occurs after Dummy1 and Dummy2 have loaded at startup. Dummy 3 doesn't load until it is made visible.
I believe the problem is that, since the template has not been applied for the collapsed Test3 instance, it has not been inserted into the visual tree. It is therefore not within the name scope of the outer Test1 instance when the binding is created, and thus cannot resolve the name ucThis specified for ElementName.
You can deal with this by adding Test3Control to the logical tree of Test2. Try modifying the dependency property definition as follows:
public static readonly DependencyProperty Test3ControlProperty =
DependencyProperty.Register("Test3Control", typeof(FrameworkElement),
typeof(Test2),
new UIPropertyMetadata(null, OnTest3ControlPropertyChanged));
private static void OnTest3ControlPropertyChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var source = (Test2)d;
var oldValue = e.OldValue as FrameworkElement;
var newValue = e.NewValue as FrameworkElement;
if (oldValue != null)
source.RemoveLogicalChild(oldValue);
if (newValue != null)
source.AddLogicalChild(newValue);
}
In most cases when you add a new UIElement-based property to a control, you will want to ensure it gets added to the logical tree. This does not happen automatically. For that matter, it does not get added to the visual tree automatically either. In this case it is only loaded into the visual tree because it is explicitly inserted into the template.
Take a look at the internals of WPF's core controls, like Decorator (from which Border derives) and its Child property to see what kind of plumbing may be required when defining new control types.
Also note how many "child control" properties are not dependency properties. It's easy to run into problems with Visual-based dependency properties, particularly when you attempt to change them via setters or animate them. I find it's best to discourage developers from misusing the properties and creating headaches for themselves by simply exposing child controls as regular CLR properties.
I am not sure about your specific problem, but I'd move the Test2's implicit style definition to the Generic.xaml module which has to be in the "Themes" folder. The framework will scan automatically that file to look for implicit styles.

Is there a way to programatically bind to InputBinding Command property?

I know it's a generic title, but my question is specific. I think it will boil down to a question of practice. So, I have the following code:
public partial class MainWindow : Window
{
InitializeComponent();
MyViewModel viewModel = new MyViewModel();
this.myGrid.DataContext = viewModel;
}
public class MyViewModel
{
public ICommand SomeCommandProperty { get { return this.someCommandProperty; }}
}
public class ComponentCollection : Panel
{
public ComponentCollection()
{
for (int i = 0; i < n; i++)
{
this.Children.Add(new Component());
}
}
}
public class Component : UIElement
{
public Component()
{
this.InputBindings.Add(new MouseBinding(SomeCommandProperty, new MouseGesture(MouseAction.LeftClick)));
}
}
I could easily aggregate the ViewModel that owns SomeCommandProperty into the Component class, but I'm currently waiving that option assuming there is another way.
Component is a child of ComponentCollection which is child of a Grid which DataContext is MyViewModel. ComponentCollection as the name suggests contains a collection of Components.
<Grid Name="myGrid">
<someNamespace:ComponentCollection x:Name="componentCollection"/>
</Grid>
It's the same scenario as the XAML below, but with TextBlock. I guess I'm trying to replicate what's being done in the XAML below programatically. Again, Component's top most ancestor's DataContext is set to ViewModel.
<Grid Name="myGrid">
<TextBlock Text="SomeText">
<TextBlock.InputBindings>
<MouseBinding Command="{Binding SomeCommandProperty}" MouseAction="LeftClick" />
</TextBlock.InputBindings>
</TextBlock>
</Grid>
Update 1
Basically, I have a custom control which inherit from a Panel which children are a collection of Component. It's not a hack, like I've mentioned, I could directly have access to SomeCommandProperty If I aggregate the ViewModel into Component. Doing so, however, feels icky. That is, having direct access to ViewModel from a Model.
I guess the question I'm asking is. Given the situation that Component's parent UIElement's DataContext is set to MyViewModel, is it possible to access SomeCommandProperty without Component owning a reference to the MyViewModel that owns SomeCommandProperty? Programatically, that is.
Using ItemsControl doesn't change the fact that I still need to bind SomeCommandProperty to each Items.
Update 2
See code above.
Update 3
Apparently, there isn't a mechanism I know of that will set the binding on the Command property of an InputBinding.
For example if my Component class were to Inherit from ButtonBase instead of UIElement, I would have the Command property to which I could easily set the binding programatically using FrameWorkElement's SetBinding. Unfortunately, I can't do this with InputBinding's Command property.
public class Component : ButtonBase
{
public Component()
{
System.Windows.Data.Binding binding = new System.Windows.Data.Binding
{
RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.FindAncestor, typeof(ComponentCollection), 1 ),
Path = new PropertyPath("DataContext.SomeCommandProperty")
};
// I can do this.
this.SetBinding(this.CommandProperty, binding);
// But I want to do something like below. Note: It's a pseudo code.
MouseBinding mouseBinding = new MouseBinding();
mouseBinding.SetBinding(mouseBinding.CommandProperty, binding);
this.InputBindings.Add(mouseBinding);
}
}
Update 4
BindingOperations.SetBinding can be used on Objects that don't have direct access to SetBinding.
Solution
MouseBinding mouseBinding = new MouseBinding();
BindingOperations.SetBinding(mouseBinding, MouseBinding.CommandProperty, binding);
this.InputBindings.Add(mouseBinding);
Use an ItemsControl for this. Don't try to hack something together yourself when there is a built-in class that already does this.
You can also access the ViewModel from a parent UI element in the Visual Tree by using a RelativeSource binding:
<ItemsControl>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="SomeText">
<TextBlock.InputBindings>
<!-- See how I'm using RelativeSource to get a hold of the DataContext of the parent ItemsControl -->
<MouseBinding Command="{Binding DataContext.SomeCommandProperty,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"
MouseAction="LeftClick" />
</TextBlock.InputBindings>
</TextBlock>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

Categories

Resources