
Question:
If I want to display something based on condition, then the simple approach is to use visibility binding:
<Something Visibility="{Binding ShowSomething, Converter=..." ... />
With this approach the visual tree is still created and can cause performance issues if Something
has complicated structure (many children, bindings, events, triggers, etc.).
A better approach is to add content via trigger:
<ContentControl>
<ContentControl.Style>
<Style TargetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding ShowSomething}" Value="SomeValue">
<Setter Property="Content">
<Setter.Value>
<Something ... />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
But that's a nightmare, agree? Having multiple of such <em>dynamic parts</em> will pollute xaml and make it hard to navigate.
<strong>Is there another way?</strong>
<hr />I am using data-templates whenever I can, but creating a dedicated Type
and actually defining data-template is too much when dynamic part simply depends on a value of property. Of course that property can be refactored into a type, which then can use its own data-template, but meh. I'd really prefer to not do this every time, too many <em>small types</em> and actual data-temples defined in xaml sounds same bad to me.
I actually like the second approach, but I'd like to improve it, e.g. by making xaml-extension or maybe custom control. I decide to ask question because: 1) I am lazy ;) 2) I am not sure what is <em>the best</em> way 3) I am sure others (xaml masters) have this problem solved already.
Answer1:Most reusable solution I can think of is to create custom control and wrap its content in a ControlTemplate
, so that it is lazy-loaded when needed.
Here's an example implementation:
[ContentProperty(nameof(Template))]
public class ConditionalContentControl : FrameworkElement
{
protected override int VisualChildrenCount => Content != null ? 1 : 0;
protected override Size ArrangeOverride(Size finalSize)
{
if (Content != null)
{
if (ShowContent)
Content.Arrange(new Rect(finalSize));
else
Content.Arrange(new Rect());
}
return finalSize;
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index > VisualChildrenCount - 1)
throw new ArgumentOutOfRangeException(nameof(index));
return Content;
}
private void LoadContent()
{
if (Content == null)
{
if (Template != null)
Content = (UIElement)Template.LoadContent();
if (Content != null)
{
AddLogicalChild(Content);
AddVisualChild(Content);
}
}
}
protected override Size MeasureOverride(Size constraint)
{
var desiredSize = new Size();
if (Content != null)
{
if (ShowContent)
{
Content.Measure(constraint);
desiredSize = Content.DesiredSize;
}
else
Content.Measure(new Size());
}
return desiredSize;
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == ShowContentProperty)
{
if (ShowContent)
LoadContent();
}
else if (e.Property == TemplateProperty)
{
UnloadContent();
Content = null;
if (ShowContent)
LoadContent();
}
}
private void UnloadContent()
{
if (Content != null)
{
RemoveVisualChild(Content);
RemoveLogicalChild(Content);
}
}
#region Dependency properties
private static readonly DependencyPropertyKey ContentPropertyKey = DependencyProperty.RegisterReadOnly(
nameof(Content),
typeof(UIElement),
typeof(ConditionalContentControl),
new FrameworkPropertyMetadata
{
AffectsArrange = true,
AffectsMeasure = true,
});
public static readonly DependencyProperty ContentProperty = ContentPropertyKey.DependencyProperty;
public static readonly DependencyProperty ShowContentProperty = DependencyProperty.Register(
nameof(ShowContent),
typeof(bool),
typeof(ConditionalContentControl),
new FrameworkPropertyMetadata
{
AffectsArrange = true,
AffectsMeasure = true,
DefaultValue = false,
});
public static readonly DependencyProperty TemplateProperty = DependencyProperty.Register(
nameof(Template),
typeof(ControlTemplate),
typeof(ConditionalContentControl),
new PropertyMetadata(null));
public UIElement Content
{
get => (UIElement)GetValue(ContentProperty);
private set => SetValue(ContentPropertyKey, value);
}
public ControlTemplate Template
{
get => (ControlTemplate)GetValue(TemplateProperty);
set => SetValue(TemplateProperty, value);
}
public bool ShowContent
{
get => (bool)GetValue(ShowContentProperty);
set => SetValue(ShowContentProperty, value);
}
#endregion
}
Note that this implementation does not unload the content once it is loaded, but merely arranges it so that it is of (0,0)
size. In order to unload the content from visual tree when it is not supposed to be shown, we need to make several modifications (this code sample is limited to modified code):
(...)
protected override int VisualChildrenCount => ShowContent && Content != null ? 1 : 0;
protected override Size ArrangeOverride(Size finalSize)
{
if (Content != null && ShowContent)
Content.Arrange(new Rect(finalSize));
return finalSize;
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index > VisualChildrenCount - 1)
throw new ArgumentOutOfRangeException(nameof(index));
return Content;
}
private void LoadContent()
{
if (Content == null && Template != null)
Content = (UIElement)Template.LoadContent();
if (Content != null)
{
AddLogicalChild(Content);
AddVisualChild(Content);
}
}
protected override Size MeasureOverride(Size constraint)
{
var desiredSize = new Size();
if (Content != null && ShowContent)
{
Content.Measure(constraint);
desiredSize = Content.DesiredSize;
}
return desiredSize;
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == ShowContentProperty)
{
if (ShowContent)
LoadContent();
else
UnloadContent();
}
else if (e.Property == TemplateProperty)
{
UnloadContent();
Content = null;
if (ShowContent)
LoadContent();
}
}
(...)
Usage example:
<StackPanel>
<CheckBox x:Name="CB" Content="Show content" />
<local:ConditionalContentControl ShowContent="{Binding ElementName=CB, Path=IsChecked}">
<ControlTemplate>
<Border Background="Red" Height="200" />
</ControlTemplate>
</local:ConditionalContentControl>
</StackPanel>
Answer2:If you don't mind the content being instantiated upon parsing the <em>XAML</em> and only want to keep it out of visual tree, here's a control that accomplishes this goal:
[ContentProperty(nameof(Content))]
public class ConditionalContentControl : FrameworkElement
{
private UIElement _Content;
public UIElement Content
{
get => _Content;
set
{
if (ReferenceEquals(value, _Content)) return;
UnloadContent();
_Content = value;
if (ShowContent)
LoadContent();
}
}
protected override int VisualChildrenCount => ShowContent && Content != null ? 1 : 0;
protected override Size ArrangeOverride(Size finalSize)
{
if (Content != null && ShowContent)
Content.Arrange(new Rect(finalSize));
return finalSize;
}
protected override Visual GetVisualChild(int index)
{
if (index < 0 || index > VisualChildrenCount - 1)
throw new ArgumentOutOfRangeException(nameof(index));
return Content;
}
private void LoadContent()
{
if (Content != null)
{
AddLogicalChild(Content);
AddVisualChild(Content);
}
}
protected override Size MeasureOverride(Size constraint)
{
var desiredSize = new Size();
if (Content != null && ShowContent)
{
Content.Measure(constraint);
desiredSize = Content.DesiredSize;
}
return desiredSize;
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == ShowContentProperty)
{
if (ShowContent)
LoadContent();
else
UnloadContent();
}
}
private void UnloadContent()
{
if (Content != null)
{
RemoveVisualChild(Content);
RemoveLogicalChild(Content);
}
}
#region Dependency properties
public static readonly DependencyProperty ShowContentProperty = DependencyProperty.Register(
nameof(ShowContent),
typeof(bool),
typeof(ConditionalContentControl),
new FrameworkPropertyMetadata
{
AffectsArrange = true,
AffectsMeasure = true,
DefaultValue = false,
});
public bool ShowContent
{
get => (bool)GetValue(ShowContentProperty);
set => SetValue(ShowContentProperty, value);
}
#endregion
}
Usage:
<StackPanel>
<CheckBox x:Name="CB" Content="Show content" />
<local:ConditionalContentControl ShowContent="{Binding ElementName=CB, Path=IsChecked}">
<Border Background="Red" Height="200" />
</local:ConditionalContentControl>
</StackPanel>
Note though that this approach has its drawbacks, e.g. bindings with relative sources will report errors if the content is not loaded immediately.
Answer3:I decided to post my attempt as an answer:
public class DynamicContent : ContentControl
{
public bool ShowContent
{
get { return (bool)GetValue(ShowContentProperty); }
set { SetValue(ShowContentProperty, value); }
}
public static readonly DependencyProperty ShowContentProperty =
DependencyProperty.Register("ShowContent", typeof(bool), typeof(DynamicContent),
new PropertyMetadata(false,
(sender, e) => ((DynamicContent)sender).ChangeContent((bool)e.NewValue)));
protected override void OnContentChanged(object oldContent, object newContent)
{
base.OnContentChanged(oldContent, newContent);
ChangeContent(ShowContent);
}
void ChangeContent(bool show) => Template = show ? (ControlTemplate)Content : null;
}
It's short, clear (is it?) and working.
The idea is to use ContentControl.Content
to specify control template and change control Template
to <em>show/hide</em> it when ShowContent
or Content
(to support design time) value is changed.
Testing example (including relative and by name bindings):
<StackPanel Tag="Test">
<CheckBox x:Name="comboBox"
Content="Show something"
IsChecked="{Binding ShowSomething}" />
<local:DynamicContent ShowContent="{Binding IsChecked, ElementName=comboBox}">
<ControlTemplate>
<local:MyCheckBox IsChecked="{Binding IsChecked, ElementName=comboBox}"
Content="{Binding Tag, RelativeSource={RelativeSource AncestorType=StackPanel}}" />
</ControlTemplate>
</local:DynamicContent>
</StackPanel>
To see what it's deferred:
public class MyCheckBox : CheckBox
{
public MyCheckBox()
{
Debug.WriteLine("MyCheckBox is constructed");
}
}