Silverlight databind int to lookless control not working - silverlight

I am working a very simple lookless control, and I can't seem to get one of the template bindings to work. In the control I have two Dependency Properties, the one that is a string works, and the one that is an int does not.
The csharp code looks like this:
using System;
using System.Windows;
using System.Windows.Controls;
namespace ControlDemo
{
public class TextControlLookless : Control
{
#region Title
public static readonly DependencyProperty ChartTitleProperty =
DependencyProperty.Register("ChartTitle", typeof(string), typeof(TextControlLookless),
null);
public String ChartTitle
{
get { return (string)GetValue(ChartTitleProperty); }
set
{
SetValue(ChartTitleProperty, value);
}
}
#endregion
#region Value
public static readonly DependencyProperty ChartValueProperty =
DependencyProperty.Register("ChartValue", typeof(int), typeof(TextControlLookless),
null);
public int ChartValue
{
get { return (int)GetValue(ChartValueProperty); }
set
{
SetValue(ChartValueProperty, value);
}
}
#endregion
#region ctor
public TextControlLookless()
{
this.DefaultStyleKey = typeof(TextControlLookless);
}
#endregion
}
}
And the xaml for the control looks like this:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:ControlDemo">
<Style TargetType="local:TextControlLookless">
<Setter Property="ChartTitle" Value="Set Title" />
<Setter Property="ChartValue" Value="1" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:TextControlLookless">
<Grid x:Name="Root">
<Border BorderBrush="Black" BorderThickness="2">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="{TemplateBinding ChartTitle}" />
<TextBlock Text="{TemplateBinding ChartValue}" Grid.Row="1" />
</Grid>
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
When I put this on a page, I can see the ChartTitle (either Set Title, or whatever I set it to), but the ChartValue never shows up. If I change its type to a string, it does show up, so I must be missing something.

The problem is that TemplateBinding is a far more primitive operation than Binding. Binding is an actual class and includes some helpful features including the implicit conversion of strings back and forth between other data types.
TemplateBinding is purely a markup instruction and crucially in your case does not do type conversion for you. Hence the dependency property being bound to a Text property of a TextBlock must be a string.
You have two choices:-
One choice is instead using TemplateBinding give the TextBlock a name and assign its Text in the ChartValue property changed call back:-
#region Value
public static readonly DependencyProperty ChartValueProperty =
DependencyProperty.Register("ChartValue", typeof(int), typeof(TextControlLookless),
new PropertyMetadata(0, OnChartValuePropertyChanged));
private static void OnChartValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
TextControlLookless source = d as TextControlLookless;
source.Refresh();
}
public int ChartValue
{
get { return (int)GetValue(ChartValueProperty); }
set
{
SetValue(ChartValueProperty, value);
}
}
#endregion
private TextBlock txtChartValue { get; set; }
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
txtChartValue = GetTemplateChild("txtChartValue") as TextBlock;
Refresh();
}
private void Refresh()
{
if (txtChartValue != null)
{
txtChartValue.Text = ChartValue.ToString();
}
}
where the xaml looks like:-
<TextBlock x:Name="txtChartValue" Grid.Row="1" />
The other choice is to create a private dependency property for the value with type of string:-
#region Value
public static readonly DependencyProperty ChartValueProperty =
DependencyProperty.Register("ChartValue", typeof(int), typeof(TextControlLookless),
new PropertyMetadata(0, OnChartValuePropertyChanged));
private static void OnChartValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
d.SetValue(ChartValueStrProperty, e.NewValue.ToString());
}
private static readonly DependencyProperty ChartValueStrProperty =
DependencyProperty.Register("ChartValueStr", typeof(string), typeof(TextControlLookless),
new PropertyMetadata("0"));
public int ChartValue
{
get { return (int)GetValue(ChartValueProperty); }
set
{
SetValue(ChartValueProperty, value);
}
}
#endregion
where the xaml looks like:-
<TextBlock Text="{TemplateBinding ChartValueStr}" Grid.Row="1" />
Note that the ChartValueStrProperty is private and I haven't bothered creating a standard .NET property to cover it. TemplateBinding actually takes the property name you assign suffixes is with "Property" then looks for a static field on the target type.
Both approaches have their strengths and weaknesses. The first approach is the more common pattern but takes a little more code and is less flexiable (the control displaying the value must be a TextBlock). The second is more flexiable and uses less code but is somewhat unorthodox.

Related

Binding DataGrid Expander when using ListCollectionView

I'm trying to get all the DataGrid Expanders to open and close using a open/close button like the example below. Problem is when I'm using a single boolean to bind to this will break when I'm opening/closing a single expander.
I'm using a ListCollectionView to set the GroupingDescription.
I guess I'm looking for a solution to somehow get the Expander to play nice?
View
<DataGrid.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander IsExpanded="{Binding Source={StaticResource proxy}, Path=Data.Expanded, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}">
<Expander.Header>
<StackPanel>
<!-- label -->
</StackPanel>
</Expander.Header>
<ItemsPresenter />
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</DataGrid.GroupStyle>
ViewModel
public bool Expanded
{
get { return _expanded; }
set { _expanded = value; OnPropertyChanged(); }
}
public ListCollectionView Items
{
get
{
return _items;
}
set
{
_items = value; OnPropertyChanged();
}
}
// logic
var items = new ListCollectionView(planninglist);
items.SortDescriptions.Add(new items.GroupDescriptions.Add(new PropertyGroupDescription("AanZet"));
items.IsLiveSorting = true;
Update
Fix based on SO answer suggested in the comments by #XAMlMAX.
public class ExpanderBehavior
{
public static readonly DependencyProperty IsExpandedProperty =
DependencyProperty.RegisterAttached("IsExpanded",
typeof(bool),
typeof(ExpanderBehavior),
new PropertyMetadata(OnChanged));
public static bool GetIsExpanded(DependencyObject obj)
{
return (bool)obj.GetValue(IsExpandedProperty);
}
public static void SetIsExpanded(DependencyObject obj, bool value)
{
obj.SetValue(IsExpandedProperty, value);
}
private static void OnChanged(DependencyObject o,
DependencyPropertyChangedEventArgs args)
{
Expander tb = o as Expander;
if (null != tb)
tb.IsExpanded = (bool)args.NewValue;
}
}
As per comment conversation.
Reason why you were experiencing issue with expander staying open even though it was bound is because when you use OneWay Binding and then click on the button it then becomes disconnected. To have a better idea on when it becomes disconnected use PresentationTraceSources.TraceLevel=High.
To overcome this, one can use an attached property to stop Binding from detaching.
Now this example is for ToggleButton but the logic should apply the same.
public class TBExtender
{
public static readonly DependencyProperty IsCheckedProperty =
DependencyProperty.RegisterAttached("IsChecked",
typeof(bool),
typeof(TBExtender),
new PropertyMetadata(OnChanged));
public static bool GetIsChecked(DependencyObject obj)
{
return (bool)obj.GetValue(IsCheckedProperty);
}
public static void SetIsChecked(DependencyObject obj, bool value)
{
obj.SetValue(IsCheckedProperty, value);
}
private static void OnChanged(DependencyObject o,
DependencyPropertyChangedEventArgs args)
{
ToggleButton tb = o as ToggleButton;
if (null != tb)
tb.IsChecked = (bool)args.NewValue;
}
}
Credit goes to alex-p
and wallstreet-programmer
for responses on this SO question.

Bind a property to DataTemplateSelector

I want to design a DataTemplateSelector who compare the given value with a one passed in parameter and choose the right template if the value is superior or inferior
I came with the following :
class InferiorSuperiorTemplateSelector : DataTemplateSelector
{
public DataTemplate SuperiorTemplate { get; set; }
public DataTemplate InferiorTemplate { get; set; }
public double ValueToCompare { get; set; }
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
double dpoint = Convert.ToDouble(item);
return (dpoint >= ValueToCompare || dpoint == null) ? SuperiorTemplate : InferiorTemplate;
}
}
and the XAML :
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30" />
<RowDefinition Height="30" />
<RowDefinition Height="30" />
</Grid.RowDefinitions>
<TextBox Name="theValue" Grid.Row="0">1</TextBox>
<ContentControl Grid.Row="2" Content="{Binding ElementName=theValue, Path=Text}" >
<ContentControl.ContentTemplateSelector>
<sel:InferiorSuperiorTemplateSelector ValueToCompare="12" SuperiorTemplate="{StaticResource posTemplate}" InferiorTemplate="{StaticResource negTemplate}" />
</ContentControl.ContentTemplateSelector>
</ContentControl>
</Grid>
This works pretty fine if valueToCompare parameter is set manually (here with 12).
When I try to make this one dynamic, by applying a binding I got the following error :
A 'Binding' cannot be set on the 'ValueToCompare' property of type
'InferiorSuperiorTemplateSelector'. A 'Binding' can only be set on a
DependencyProperty of a DependencyObject.
And here comes the problem : how can we declare a DependencyProperty in a DataTemplateSelector or is there any other option to acheve this goal ?
I tried to define a dependencyproperty using the usual way but I can't resole the SetValue and GetValue methods.
Thanks by advance.
EDIT : As an appendix of the solution mentionned above, here is the fixed XAML code of my sample.
<TextBox Name="theValue" Grid.Row="0">1</TextBox>
<TextBox Name="theValueToCompare" Grid.Row="1">50</TextBox>
<ContentControl Grid.Row="2" Content="{Binding ElementName=theValue, Path=Text}"
local:DataTemplateParameters.ValueToCompare="{Binding ElementName=theValueToCompare, Path=Text}">
<ContentControl.ContentTemplateSelector>
<local:InferiorSuperiorTemplateSelector SuperiorTemplate="{StaticResource posTemplate}" InferiorTemplate="{StaticResource negTemplate}" />
</ContentControl.ContentTemplateSelector>
</ContentControl>
The other parts of the code are similar.
As evident from the error you can only bind with dependency property.
But since it's already inheriting from DataTemplateSelector, you cannot inherit from DependencyObject class.
So, I would suggest to create an Attached property for binding purpose. But catch is attached property can only be applied on class deriving from DependencyObject.
So, you need to tweak a bit to get it working for you. Let me explain step by step.
First - Create attached property as suggested above:
public class DataTemplateParameters : DependencyObject
{
public static double GetValueToCompare(DependencyObject obj)
{
return (double)obj.GetValue(ValueToCompareProperty);
}
public static void SetValueToCompare(DependencyObject obj, double value)
{
obj.SetValue(ValueToCompareProperty, value);
}
public static readonly DependencyProperty ValueToCompareProperty =
DependencyProperty.RegisterAttached("ValueToCompare", typeof(double),
typeof(DataTemplateParameters));
}
Second - Like I said it can be set only on object deriving from DependencyObject, so set it on ContentControl:
<ContentControl Grid.Row="2" Content="{Binding Path=PropertyName}"
local:DataTemplateParameters.ValueToCompare="{Binding DecimalValue}">
<ContentControl.ContentTemplateSelector>
<local:InferiorSuperiorTemplateSelector
SuperiorTemplate="{StaticResource SuperiorTemplate}"
InferiorTemplate="{StaticResource InferiorTemplate}" />
</ContentControl.ContentTemplateSelector>
</ContentControl>
Third. - Now you can get the value inside template from container object passed as parameter. Get Parent (ContentControl) using VisualTreeHelper and get value of attached property from it.
public override System.Windows.DataTemplate SelectTemplate(object item,
System.Windows.DependencyObject container)
{
double dpoint = Convert.ToDouble(item);
double valueToCompare = (double)VisualTreeHelper.GetParent(container)
.GetValue(DataTemplateParameters.ValueToCompareProperty); // HERE
// double valueToCompare = (container as FrameworkElement).TemplatedParent;
return (dpoint >= valueToCompare) ? SuperiorTemplate : InferiorTemplate;
}
Also you can get ContentControl like this (container as FrameworkElement).TemplatedParent.

How to add events to templated control in silverlight?

Bounty Rewarded for any solid tutorial/learning resources regarding wiring up events with templated controls.
I Have a control template like this:
<Style TargetType="local:DatePicker">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DatePicker">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" x:Name="myDatePickerContentArea">
<StackPanel Orientation="Vertical">
<Button x:Name="myTestButton" Content="Test button" />
<telerik:RadDatePicker Style="{StaticResource VisitsReportTextBoxStyle}" Foreground="#FFFFFF" x:Name="startDate" DateTimeWatermarkContent="Start Date"/>
<telerik:RadDatePicker Style="{StaticResource VisitsReportTextBoxStyle}" x:Name="endDate" DateTimeWatermarkContent="End Date"/>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
The C# for this template is:
public class DatePicker : Control
{
public static readonly DependencyProperty StartDateSelectedDateProperty = DependencyProperty.Register("StartDateSelectedDateProperty", typeof(DateTime), typeof(DatePicker), null);
public DateTime? StartDateSelectedDate { get; set; }
public DatePicker()
{
this.DefaultStyleKey = typeof(DatePicker);
}
public override void OnApplyTemplate()
{
RadDatePicker StartDate = this.GetTemplateChild("startDate") as RadDatePicker;
StartDate.SelectionChanged += new Telerik.Windows.Controls.SelectionChangedEventHandler(StartDate_SelectionChanged);
StartDate.SelectedDate = new DateTime(2010, 01, 01);
base.OnApplyTemplate();
}
void StartDate_SelectionChanged(object sender, Telerik.Windows.Controls.SelectionChangedEventArgs e)
{
RadDatePicker temp = (RadDatePicker)sender;
StartDateSelectedDate = temp.SelectedDate;
}
}
My selectionChanged Event Doesn't Fire and I'm not sure why.
Any Ideas ?
Here is an example of using best practice with the sort of control I think you are attempting to build (see notes at end for some explanations):-
[TemplatePart(Name = DatePicker.ElementStartDate, Type = typeof(RadDatePicker))]
[TemplatePart(Name = DatePicker.ElementEndDate, Type = typeof(RadDatePicker))]
public class DatePicker : Control
{
public DatePicker()
{
this.DefaultStyleKey = typeof(DatePicker);
}
#region Template Part Names
private const string ElementStartDate = "startDate";
private const string ElementEndDate = "endDate";
#endregion
#region Template Parts
private RadDatePicker _StartDate;
internal RadDatePicker StartDate
{
get { return _StartDate; }
private set
{
if (_StartDate != null)
{
_StartDate.SelectionChanged -= StartDate_SelectionChanged;
}
_StartDate = value;
if (_StartDate != null)
{
_StartDate.SelectionChanged += StartDate_SelectionChanged;
}
}
}
private RadDatePicker _EndDate;
internal RadDatePicker EndDate
{
get { return _EndDate; }
private set
{
if (_EndDate!= null)
{
_EndDate.SelectionChanged -= EndDate_SelectionChanged;
}
_EndDate= value;
if (_EndDate!= null)
{
_EndDate.SelectionChanged += EndDate_SelectionChanged;
}
}
}
#endregion
public static readonly DependencyProperty StartDateSelectedDateProperty =
DependencyProperty.Register(
"StartDateSelectedDateProperty",
typeof(DateTime?),
typeof(DatePicker),
new PropertyMetaData(new DateTime(2010, 01, 01)));
public DateTime? StartDateSelectedDate
{
get { return (DateTime?)GetValue(StartDateSelectedDateProperty); }
set { SetValue(StartDateSelectedDateProperty)}
}
public static readonly DependencyProperty EndDateSelectedDateProperty =
DependencyProperty.Register(
"EndDateSelectedDateProperty",
typeof(DateTime?),
typeof(DatePicker),
new PropertyMetaData(new DateTime(2010, 01, 01)));
public DateTime? EndDateSelectedDate
{
get { return (DateTime?)GetValue(EndDateSelectedDateProperty); }
set { SetValue(EndDateSelectedDateProperty)}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
StartDate = GetTemplateChild(ElementStartDate) as RadDatePicker;
EndDate = GetTemplateChild(ElementEndDate) as RadDatePicker;
}
void StartDate_SelectionChanged(object sender, Telerik.Windows.Controls.SelectionChangedEventArgs e)
{
// Do stuff with StartDate here
}
void EndDate_SelectionChanged(object sender, Telerik.Windows.Controls.SelectionChangedEventArgs e)
{
// Do stuff with EndDate here
}
}
The template Xaml should look like:-
<Style TargetType="local:DatePicker">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:DatePicker">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}" x:Name="myDatePickerContentArea">
<StackPanel Orientation="Vertical">
<Button x:Name="myTestButton" Content="Test button" />
<telerik:RadDatePicker x:Name="startDate"
Style="{StaticResource VisitsReportTextBoxStyle}"
Foreground="#FFFFFF"
DateTimeWatermarkContent="Start Date"
SelectedDate="{TemplateBinding StartDateSelectedDate}"
/>
<telerik:RadDatePicker x:Name="endDate"
Style="{StaticResource VisitsReportTextBoxStyle}"
DateTimeWatermarkContent="End Date"
SelectedDate="{TemplateBinding EndDateSelectedDate}"
/>
</StackPanel>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Some Explanations
A key problem your original code had was that it hadn't implemented the dependency properties correctly. Note that properties now use GetValue and SetValue, also that the property meta data is used to assign the default rather than attempting to set in in onapplytemplate.
With the properties correctly implemented the template binding should work and in fact we're done as far as getting what appears to be your original intent, hence I've left out any actual code in the event handlers.
Create constants in the code to hold the names of key template parts that you want to interact with, this allows for name changes to be less expensive to make.
Add TemplatePart attributes to the class to indicate the key elements that the code expects to find, what their names should be and what base type they are expected to have. This allows for a designer to re-template an existing control, as long the declared template parts are present somewher the control should function correctly even if its UI is radically altered.
If you need to attach event handlers for some elements create a field to hold a reference to the element and then create a property to wrap round it. The property setter should then detach and attach the event handlers as you see in the code.
Make sure bae.OnApplyTemplate is called in the override of OnApplyTemplate then as you can see its quite straight forward to assign the above created properties.
I don't have the RadDatePicker so I can't test, my only outstanding concern is where the DateTime? is the correct type for the SelectedDate property. Certainly if it is its an improvement over the Microsoft offering which seems to have drop the ball on this typical data entry requirement.
I may only guess that the problem is that for OnApplyTemplate method Implementers should always call the base implementation before their own implementation.
The other thing is that from your code it looks like it's better to use TemplateBinding(Archive)(V4) in the template xaml
<telerik:RadDatePicker SelectedDate={TemplateBinding StartDateSelectedDate}
Style="{StaticResource VisitsReportTextBoxStyle}"
Foreground="#FFFFFF" x:Name="startDate"
DateTimeWatermarkContent="Start Date"/>

Nested Binding of UserControls

I am having trouble getting the following scenario to work (this code is not the actual code but the principals are the same. Basically I need to pass a value down from a MainPage down to a nested "reusable user control" that binds to it's own properties. I want to see the "This is it!" text on the screen but it's not being set in the SilverlightControl2 control (I suspect due to the setting of the DataContext) - but I how do I fix it?
MainPage.xaml
<Grid>
<ContentPresenter>
<ContentPresenter.Content>
<Local:SilverlightControl1 OneValue="This is it!"/>
</ContentPresenter.Content>
</ContentPresenter>
</Grid>
SilverlightControl1.xaml
<Grid>
<Local:SilverlightControl2 TwoValue="{Binding OneValue}"/>
</Grid>
SilverlightControl1.xaml.cs
public partial class SilverlightControl1 : UserControl
{
public string OneValue
{
get { return (string)GetValue(OneValueProperty); }
set { SetValue(OneValueProperty, value); }
}
public static readonly DependencyProperty OneValueProperty = DependencyProperty.Register(
"OneValue", typeof(string), typeof(SilverlightControl1), new PropertyMetadata(string.Empty));
public SilverlightControl1()
{
InitializeComponent();
this.DataContext = this;
}
}
SilverlightControl2.xaml
<Grid x:Name="LayoutRoot" Background="White">
<TextBlock Text="{Binding TwoValue}" Foreground="Blue" />
</Grid>
SilverlightControl2.xaml.cs
public partial class SilverlightControl2 : UserControl
{
public string TwoValue
{
get { return (string)GetValue(TwoValueProperty); }
set { SetValue(TwoValueProperty, value); }
}
public static readonly DependencyProperty TwoValueProperty = DependencyProperty.Register(
"TwoValue", typeof(string), typeof(SilverlightControl2), new PropertyMetadata(string.Empty));
public SilverlightControl2()
{
InitializeComponent();
this.DataContext = this;
}
}
As soon as you find yourself feeling the need to do this:-
this.DataContext = this;
know that you have probably got things wrong. Its probably the first thing I would expect to find on Silverlight specific "bad smell list".
In this case where you are specialising UserControl a better approach is to do this:-
SilverlightControl1.xaml
<Grid>
<Local:SilverlightControl2 x:Name="MyControl2" />
</Grid>
SilverlightControl1.xaml.cs (I'm just showing the constructor the rest is as you have it)
public SilverlightControl1()
{
InitializeComponent();
MyControl2.SetBinding(SilverlightControl2.TwoValueProperty , new Binding("OneValue") { Source = this });
}
SilverlightControl2.xaml
<Grid x:Name="LayoutRoot" Background="White">
<TextBlock x:Name="MyTextBox" Foreground="Blue" />
</Grid>
SilverlightControl1.xaml.cs (I'm just showing the constructor the rest is as you have it)
public SilverlightControl2()
{
InitializeComponent();
MyTextBox.SetBinding(TextBox.TextProperty , new Binding("TwoValue") { Source = this });
}
Since in UserControls you know the structure of the XAML and you can name the elements that you need access to in code, you can create the binding using a line of code instead.
This leaves the DataContext free to do what is designed for rather than be hi-jacked for a different purpose.
The alternative approach where instead of specialising UserControl you create a templated control, in this case the binding can be expressed in XAML using something like:-
<TextBox Text="{TemplateBinding TwoValue}" />
Template binding only works in ControlTemplate so you can't use it in a UserControl.

WPF: Why is DataContextChanged not raised on a logical child?

I have a problem with DataContextChanged not being raised on a logical child of my custom Panel control. I've narrowed it down to this:
Starting from a wizard-generated WPF application I add:
private void Window_Loaded( object sender, RoutedEventArgs e )
{
var elt = new FrameworkElement();
this.AddLogicalChild( elt );
DataContext = 42;
Debug.Assert( (int)elt.DataContext == 42 );
}
As I understand, this works because the DataContext is an inheritable dependency property.
Now, I add event handlers for DataContextChanged both on the Window (this) and its logical child:
this.DataContextChanged +=
delegate { Debug.WriteLine( "this:DataContextChanged" ); };
elt.DataContextChanged +=
delegate { Debug.WriteLine( "elt:DataContextChanged" ); };
If I run this, only the first event handler will execute. Why is this? If instead of AddLogicalChild( elt ) I do the following:
this.Content = elt;
both handlers will execute. But this is not an option in my case - I'm adding FrameworkContentElements to my control which aren't supposed to be visual children.
What's going on here? Should I do something more besides AddLogicalChild() to make it work?
(Fortunately, there is a rather simple workaround - just bind the DataContext of the element to the DataContext of the window)
BindingOperations.SetBinding( elt, FrameworkElement.DataContextProperty,
new Binding( "DataContext" ) { Source = this } );
Thank you.
You need to override the LogicalChildren property too:
protected override System.Collections.IEnumerator LogicalChildren
{
get { yield return elt; }
}
Of course, you'll want to return any logical children defined by the base implementation, too.
I'd like to add some advice to Kent's answer if someone runs into a similar problems:
If you create a custom control with multiple content objects you should ensure:
Content objects are added to the LogicalTree via AddLogicalChild()
Create your own Enumerator class and return an instance of that in the overriden LogicalChildren property
If you don't add the content objects to the logical tree you might run into problems like Bindings with ElementNames can not be resolved (ElementName is resolved by FindName which in turn uses the LogicalTree to find the elements).
What makes it even more dangerous is that my experience is that if you miss to add the objects to the logical tree the ElementName resolving works in some scenarios and it doesn't work in other scenarios.
If you don't override LogicalChildren the DataContext is not updated like described above.
Here a short example with a simple SplitContainer:
SplitContainer:
public class SplitContainer : Control
{
static SplitContainer()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(SplitContainer), new FrameworkPropertyMetadata(typeof(SplitContainer)));
}
/// <summary>
/// Identifies the <see cref="Child1"/> property.
/// </summary>
public static readonly DependencyProperty Child1Property =
DependencyProperty.Register(nameof(Child1), typeof(object), typeof(SplitContainer), new FrameworkPropertyMetadata(Child1PropertyChangedCallback));
/// <summary>
/// Left Container
/// </summary>
public object Child1
{
get { return (object)GetValue(Child1Property); }
set { SetValue(Child1Property, value); }
}
/// <summary>
/// Identifies the <see cref="Child2"/> property.
/// </summary>
public static readonly DependencyProperty Child2Property =
DependencyProperty.Register(nameof(Child2), typeof(object), typeof(SplitContainer), new FrameworkPropertyMetadata(Child2PropertyChangedCallback));
/// <summary>
/// Right Container
/// </summary>
public object Child2
{
get { return (object)GetValue(Child2Property); }
set { SetValue(Child2Property, value); }
}
private static void Child1PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var splitContainer = (SplitContainer)d;
if (e.OldValue != null)
{
splitContainer.RemoveLogicalChild(e.OldValue);
}
if (e.NewValue != null)
{
splitContainer.AddLogicalChild(((object)e.NewValue));
}
}
private static void Child2PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var splitContainer = (SplitContainer)d;
if (e.OldValue != null)
{
splitContainer.RemoveLogicalChild(e.OldValue);
}
if (e.NewValue != null)
{
splitContainer.AddLogicalChild(((object)e.NewValue));
}
}
protected override IEnumerator LogicalChildren
{
get
{
return new SplitContainerLogicalChildrenEnumerator(this);
}
}
}
SplitContainerLogicalChildrenEnumerator:
internal class SplitContainerLogicalChildrenEnumerator : IEnumerator
{
private readonly SplitContainer splitContainer;
private int index = -1;
public SplitContainerLogicalChildrenEnumerator(SplitContainer splitContainer)
{
this.splitContainer = splitContainer;
}
public object Current
{
get
{
if (index == 0)
{
return splitContainer.Child1;
}
else if (index == 1)
{
return splitContainer.Child2;
}
throw new InvalidOperationException("No child for this index available");
}
}
public bool MoveNext()
{
index++;
return index < 2;
}
public void Reset()
{
index = -1;
}
}
Style (e.g. in Themes/generic.xaml):
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:scm="clr-namespace:System.ComponentModel;assembly=PresentationFramework"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:SplitContainerElementNameProblem">
<Style TargetType="{x:Type local:SplitContainer}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:SplitContainer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentPresenter Grid.Column="0"
Content="{TemplateBinding Child1}" />
<ContentPresenter Grid.Column="1"
Content="{TemplateBinding Child2}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Sample which demonstrates that each Binding works fine:
XAML:
<Window x:Class="SplitContainerElementNameProblem.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SplitContainerElementNameProblem"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0">
<TextBox x:Name="text1" Text="abc" />
</Grid>
<local:SplitContainer Grid.Row="1">
<local:SplitContainer.Child1>
<TextBox x:Name="text2"
Text="{Binding ElementName=text1, Path=Text}" />
</local:SplitContainer.Child1>
<local:SplitContainer.Child2>
<StackPanel>
<TextBox x:Name="text3"
Text="{Binding ElementName=text2, Path=Text}" />
<TextBox x:Name="text4"
Text="{Binding MyName}" />
</StackPanel>
</local:SplitContainer.Child2>
</local:SplitContainer>
</Grid>
XAML.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = this;
MyName = "Bruno";
}
public string MyName
{
get;
set;
}
}

Resources