1

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 dynamic parts will pollute xaml and make it hard to navigate.

Is there another way?


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 small types 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 the best way 3) I am sure others (xaml masters) have this problem solved already.

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • 1
    Have you considered using `DataTemplateSelector`? – Alex Seleznyov Jan 29 '18 at 11:05
  • @AlexSeleznyov, I am often using them. It's not something what can be used in xaml directly (you need container element what supports it, e.g. `ItemsControl.ItemTemplateSelector`) and it's typically very specific, either to UI element type or underlying data type. If you have an answer, what looks *prettier* in xaml and allows to bind **any** property to **any** value to display **any** content, then post it please. That's what my question is about. Ideally I want one-liner of `Visibility` approach in xaml without disadvantages. – Sinatr Jan 29 '18 at 11:15
  • 1
    "*you need a container element that supports it*" - ContentControl does that already. Just set its ContentTemplateSelector. – Clemens Jan 29 '18 at 11:32
  • Alternatively, make your converter return Something: `` – Clemens Jan 29 '18 at 11:34
  • Btw, [this post](https://stackoverflow.com/questions/10751419/contentcontrol-with-datatemplateselector-help-needed) has sample code and is takes far less lines than based on `DataTrigger`. – Alex Seleznyov Jan 29 '18 at 11:34
  • @Clemens the converter is the not the option, according to topic starter. And I believe it would require one type per template as well, which is again against topic starter's wishes. – Alex Seleznyov Jan 29 '18 at 11:37
  • @Clemens it looks as if I didn't get your idea. Is that to return a `FrameworkElement` directly from converter, not some data object to find `DataTemplate` for? – Alex Seleznyov Jan 29 '18 at 11:49
  • @Clemens, I am curious what you mean with converter. Can you check answers and if tell me (or post the answer) if your solution is better (usually your solutions **are** better ;) )? – Sinatr Jan 29 '18 at 15:24
  • Just a simple IValueConverter with a Convert method that returns an appropriate UI element, e.g. your Something. – Clemens Jan 29 '18 at 15:28
  • @Clemens, but "something" has to be defined in xaml, it will be different from one dynamic part to another. I was thinking about dependency property which you set, but that would require full xaml syntax to assign binding (multiple lines). – Sinatr Jan 29 '18 at 15:31
  • You may perhaps load it with XamlReader. – Clemens Jan 29 '18 at 15:43

3 Answers3

1

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>
Grx70
  • 10,041
  • 1
  • 40
  • 55
  • Pretty impressive. I was trying to achieve the same and my attempt is worse in many ways (using `ContentControl` as base). I am going to try it and in a meantime few questions: Why do you include part without unloading into the answer (delete it?)? Is it possible to somehow omit using `ControlTemplate` tag in xaml (minus 2 lines)? I am not sure, but e.g. `Popup` doesn't (does it?) loads its content and you don't have to write `ControlTemplate` there (though I might be wrong, have to test it). – Sinatr Jan 29 '18 at 13:39
  • 1. I threw it in because unloading and reloading the content into the visual tree can also be expensive operation, so you have an alternative. 2. If you don't mind the content being instantiated upon parsing the _XAML_ and only wan't to defer loading it into the visual tree, then sure it's possible (and quite easy - i'll post it as another answer). Otherwise it would be tricky (would require creating custom deferring loader to use with `XamlDeferLoadAttribute`), but I think it still would be possible. – Grx70 Jan 29 '18 at 13:53
  • I tested `Popup`, I was wrong it loads everything. If it's possible than it would be much better without `ControlTemplate`. – Sinatr Jan 29 '18 at 14:09
0

If you don't mind the content being instantiated upon parsing the XAML 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.

Grx70
  • 10,041
  • 1
  • 40
  • 55
  • I personally don't want it. What is the difference with binding to `Visibility` then? But maybe someone will find it useful, thanks. – Sinatr Jan 29 '18 at 14:38
  • The difference is that if you set `Visibility` to `Collapsed`, the element, despite not being visible, is still part of the visual tree. – Grx70 Jan 29 '18 at 14:43
  • If I use dynamic content for a list of items, then that "loading" is very unwanted. I'd prefer deferred (lazy) loading, that's the main reason why data trigger is used. Presence in visual tree wasn't the best criteria to explain the problem. – Sinatr Jan 29 '18 at 14:54
0

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 show/hide 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");
    }
}
Sinatr
  • 20,892
  • 15
  • 90
  • 319