Search code examples
wpfcachingwpf-controlsdatatemplate

How to reuse controls instances in DataTemplate?


I have few layouts (different DataTemplates) with video controls. Creation of this video controls is very long. I want to reuse instances of this video controls in different DataTemplates.

Farfetched example:

Codebehind and ViewModel:

public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Layout1 = (DataTemplate)this.FindResource("_layout1");
            Layout2 = (DataTemplate)this.FindResource("_layout2");

            DataContext = new ViewModel {Content1 = "Content1", Content2 = "Content2"};
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            _view.ContentTemplate = _view.ContentTemplate == Layout1 ? Layout2 : Layout1;
        }

        DataTemplate Layout1;
        DataTemplate Layout2;
    }

    public class ViewModel
    {
        public string Content1 { get; set; }
        public string Content2 { get; set; }
    }

XAML

<Window Name="_mainForm" x:Class="WpfVideo.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:model="clr-namespace:WpfVideo"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <DataTemplate x:Key="_layout1" DataType="{x:Type model:ViewModel}">
        <StackPanel>
            <Button Content="{Binding Content1}"/>
            <Button Content="{Binding Content2}"/>
        </StackPanel>   
    </DataTemplate>

    <DataTemplate x:Key="_layout2" DataType="{x:Type model:ViewModel}">
        <StackPanel Orientation="Horizontal">
            <Button Content="{Binding Content1}"/>
            <Button Content="{Binding Content2}"/>
        </StackPanel>
    </DataTemplate>
</Window.Resources>

<StackPanel>
    <Button Click="Button_Click">Change</Button>
    <ContentPresenter Name="_view" Content="{Binding}" ContentTemplate="{StaticResource _layout1}"/>
</StackPanel>

How to reuse Buttons and prevent creation of new buttons on each template change?

EDITED: Used switch between datatemplates _layout1 and _layout2 on Button_click. Only one template active. I don’t want to draw one instance of control in two places. I want to prevent creation of controls in other template when it activated (previous deactivated). Or maybe I can use another approach, without datatemplates? Style, resources, triggers?


Solution

  • Controls Pool

    ControlsPoolControl<T> 
    IControlsPool<T>
    
    • T – type of reusable control.
    • IControlsPool - used to store reusable instances of control by key object (some property of viewmodel)
    • ControlsPoolControl - Container, which restore inner control from IControlsPool by binded key.

    Implementation

    ControlsPoolControl

    public class ControlsPoolControl<T> : UserControl where T : UIElement
    {
        private readonly Panel _mainPanel;
        private T _innerControl;
    
        public ControlsPoolControl()
        {
            _mainPanel = new Grid();
            Content = _mainPanel;
        }
    
        #region Properties
    
        #region DependencyProperty
    
        public static readonly DependencyProperty KeyObjectProperty = DependencyProperty.Register("KeyObject", typeof(object), typeof(ControlsPoolControl<T>), new PropertyMetadata(null, KeyObjectChanged));
        public static readonly DependencyProperty PoolProperty = DependencyProperty.Register("Pool", typeof(IControlsPool<T>), typeof(ControlsPoolControl<T>), new PropertyMetadata(null, PoolChanged));
    
        #endregion
    
        public object KeyObject
        {
            get { return GetValue(KeyObjectProperty); }
            set { SetValue(KeyObjectProperty, value); }
        }
    
        public IControlsPool<T> Pool
        {
            get { return (IControlsPool<T>)GetValue(PoolProperty); }
            set { SetValue(PoolProperty, value); }
        }
    
        protected T InnerControl
        {
            get { return _innerControl; }
            set
            {
                if (_innerControl == value)
                    return;
    
                _innerControl = value;
                OnControlChanged();
            }
        }
    
        #endregion
    
        #region Private API
    
        void Clear()
        {
            _mainPanel.Children.Clear();
        }
    
        void OnKeyObjectChanged()
        {
            UpdateControl();
        }
    
        void OnControlChanged()
        {
            VerifyAccess();
            Clear();
    
            var ctrl = InnerControl;
            if (ctrl != null)
                _mainPanel.Children.Add(ctrl);
        }
    
        private void UpdateControl()
        {
            if (KeyObject == null || Pool == null)
                InnerControl = null;
            else
                InnerControl = Pool.Get(KeyObject);
        }
    
        private static void KeyObjectChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ControlsPoolControl<T>)d).OnKeyObjectChanged();
        }
    
        private static void PoolChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((ControlsPoolControl<T>)d).UpdateControl();
        }
    
        #endregion
    }
    

    ControlsPool

    public interface IControlsPool<T> where T : UIElement
    {
        void Add(object key, T control);
        T Get(object key);
    }
    
    public class ControlsPool<T> : IControlsPool<T> where T : UIElement
    {
        readonly IDictionary<object, T> _controls = new Dictionary<object, T>();
    
        public void Add(object key, T control)
        {
            if (key == null) throw new ArgumentNullException("key");
    
            if (_controls.ContainsKey(key)) return;
            _controls.Add(key, control);
        }
    
        public T Get(object key)
        {
            if (key == null) throw new ArgumentNullException("key");
    
            T control = null;
            if (!_controls.TryGetValue(key, out control))
            {
                control = CreateInstance(key);
                _controls.Add(key, control);
            }
    
            return control;
        }
    
        protected virtual T CreateInstance(object key)
        {
            return Activator.CreateInstance<T>();
        }
    }
    

    Usage

    Codebehind

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
    
            ContentControlType = typeof(MyButton);
    
            Layout1 = (DataTemplate)FindResource("_layout1");
            Layout2 = (DataTemplate)FindResource("_layout2");
    
            DataContext = new ViewModel { Content1 = "Content1", Content2 = "Content2" };
        }
    
        public Type ContentControlType { get; set; }
    
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            _view.ContentTemplate = _view.ContentTemplate == Layout1 ? Layout2 : Layout1;
        }
    
        DataTemplate Layout1;
        DataTemplate Layout2;
    }
    
    public class ViewModel
    {
        public string Content1 { get; set; }
        public string Content2 { get; set; }
    }
    
    public class ButtonsPool : ControlsPool<MyButton> { }
    
    public class ButtonPoolControl : ControlsPoolControl<MyButton>
    {
    }
    
    public class MyButton : Button
    {
        static int _counter = 0;
    
        public MyButton()
        {
            _counter++;
        }
    }
    

    XAML To initialize reusable control, you should use style.

    <Window Name="_mainForm" x:Class="WpfVideo.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:model="clr-namespace:WpfVideo" 
            Title="MainWindow" Height="350" Width="525">
        <Window.Resources>
    
            <model:ButtonsPool x:Key="_buttonsPool"/>
            
            <DataTemplate x:Key="_layout1" DataType="{x:Type model:ViewModel}">
                <StackPanel>
                    <model:ButtonPoolControl KeyObject="{Binding Content1}" Pool="{StaticResource _buttonsPool}">
                        <model:ButtonPoolControl.Resources>
                            <Style TargetType="model:MyButton">
                                <Setter Property="Content" Value="{Binding Content1}"/>
                            </Style>
                        </model:ButtonPoolControl.Resources>
                    </model:ButtonPoolControl>
                    <model:ButtonPoolControl KeyObject="{Binding Content2}" Pool="{StaticResource _buttonsPool}">
                        <model:ButtonPoolControl.Resources>
                            <Style TargetType="model:MyButton">
                                <Setter Property="Content" Value="{Binding Content2}"/>
                            </Style>
                        </model:ButtonPoolControl.Resources>
                    </model:ButtonPoolControl>
                </StackPanel>
            </DataTemplate>
    
            <DataTemplate x:Key="_layout2" DataType="{x:Type model:ViewModel}">
                <StackPanel Orientation="Horizontal">
                    <model:ButtonPoolControl Pool="{StaticResource _buttonsPool}" KeyObject="{Binding Content1}">
                        <model:ButtonPoolControl.Resources>
                            <Style TargetType="model:MyButton">
                                <Setter Property="Content" Value="{Binding Content1}"/>
                            </Style>
                        </model:ButtonPoolControl.Resources>
                    </model:ButtonPoolControl>
                    <model:ButtonPoolControl Pool="{StaticResource _buttonsPool}" KeyObject="{Binding Content2}">
                        <model:ButtonPoolControl.Resources>
                            <Style TargetType="model:MyButton">
                                <Setter Property="Content" Value="{Binding Content2}"/>
                            </Style>
                        </model:ButtonPoolControl.Resources>
                    </model:ButtonPoolControl>
                </StackPanel>
            </DataTemplate>
        </Window.Resources>
    
        <StackPanel>
            <Button Click="Button_Click">Change</Button>
            <ContentPresenter Name="_view" Content="{Binding}" ContentTemplate="{StaticResource _layout1}"/>
        </StackPanel>
    </Window>