Search code examples
c#wpf-controlsitemtemplate

Specify default ItemTemplate


I have a WPF custom Control in which I have a Listview. The control has dependancy properties for the ItemSource and ItemTemplate of the ListView. This all works fine. What I would like to do is to be able to set a default ItemTemplate so that I don't end up with object.ToString() for the Items in the Listview.

Below is the XAML Style for my control.

    <ResourceDictionary 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:btl="clr-namespace:Btl.Controls"
                    mc:Ignorable="d">
    
    <DataTemplate x:Key="DefaultListViewItem" DataType="btl:SelectableItem">
        <StackPanel Orientation="Horizontal">
            <CheckBox Margin="2" IsChecked="{Binding Selected}" />
            <TextBlock Margin="5,2" Text="{Binding Description}" VerticalAlignment="Center"/>
        </StackPanel>
    </DataTemplate>
    <Style TargetType="{x:Type btl:SelectItemsControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type btl:SelectItemsControl}">
                    <Grid>
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="*"/>
                            <RowDefinition Height="Auto"/>
                        </Grid.RowDefinitions>
                        <TextBlock x:Name="PART_Title" Margin="10"
                                   Text="{Binding Path=Title,
                                            RelativeSource={RelativeSource TemplatedParent}}"
                                   TextWrapping="Wrap" 
                                   Visibility="Collapsed"/>
                        <GroupBox Grid.Row="2" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
                            <GroupBox.Header>
                                <StackPanel Orientation="Horizontal">
                                    <CheckBox x:Name="PART_EnabledCheck"  Margin="0,5" 
                                              Content="" 
                                              IsChecked="{Binding Path=EnabledCheck, Mode=TwoWay, 
                                               RelativeSource={RelativeSource TemplatedParent}}"/>
                                    <TextBlock x:Name="PART_GroupTitle" VerticalAlignment="Center"
                                               Text="{Binding Path=GroupTitle,
                                            RelativeSource={RelativeSource TemplatedParent}}"/>
                                </StackPanel>
                            </GroupBox.Header>
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="*"/>
                                    <RowDefinition Height="Auto"/>
                                </Grid.RowDefinitions>
                                <DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
                                    <ListView x:Name="PART_Items" 
                                              ItemsSource="{Binding ItemSourceList, 
                                                    RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type btl:SelectItemsControl}}}"
                                              ItemTemplate="{Binding ItemTemplate, 
                                                    RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type btl:SelectItemsControl}}}" 
                                              HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Margin="2"
                                              >
                                    </ListView>
                                </DockPanel>
                                <CheckBox x:Name="PART_SelectAllCheck" Grid.Row="1" Margin="11,5" Content="Select All" 
                                          IsChecked="{Binding Selected}"/>
                            </Grid>
                        </GroupBox>

                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

Here is my control

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Media;

    namespace Btl.Controls
    {
    /// <summary>
    /// 
    /// </summary>
    /// 
    [TemplatePart(Name = "PART_Title", Type = typeof(TextBlock))]
    [TemplatePart(Name = "PART_EnabledCheck", Type = typeof(CheckBox))]
    [TemplatePart(Name = "PART_GroupTitle", Type = typeof(TextBlock))]
    [TemplatePart(Name = "PART_Items", Type = typeof(ListView))]
    [TemplatePart(Name = "PART_SelectAllCheck", Type = typeof(CheckBox))]
    public class SelectItemsControl : UserControl
    {

        #region DependencyProperties

        #region Title
        public string Title
        {
            get { return (string)GetValue(TitleProperty); }
            set { SetValue(TitleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Title.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty TitleProperty =
            DependencyProperty.Register("Title", typeof(string), typeof(SelectItemsControl), new PropertyMetadata(string.Empty,
                OnTitleChanged
                ));

        private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {

            var control = d as TextBlock;
            if (control != null)
                if (string.IsNullOrEmpty(e.NewValue.ToString()))
            {
                control.Visibility = string.IsNullOrEmpty(e.NewValue.ToString()) ? Visibility.Collapsed : Visibility.Visible;
            }

        }


        #endregion

        #region HasEnabledCheck
        public bool HasEnabledCheck
        {
            get { return (bool)GetValue(HasEnabledCheckProperty); }
            set { SetValue(HasEnabledCheckProperty, value); }
        }

        // Using a DependencyProperty as the backing store for EnabledCheck.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HasEnabledCheckProperty =
            DependencyProperty.Register("HasEnabledCheck", typeof(bool), typeof(SelectItemsControl), new UIPropertyMetadata(false));
        #endregion

        #region EnabledCheck
        public bool EnabledCheck
        {
            get { return (bool)GetValue(EnabledCheckProperty); }
            set { SetValue(EnabledCheckProperty, value); }
        }

        // Using a DependencyProperty as the backing store for EnabledCheck.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty EnabledCheckProperty =
            DependencyProperty.Register("EnabledCheck", typeof(bool), typeof(SelectItemsControl), new UIPropertyMetadata(true));
        #endregion

        #region GroupTitle
        public string GroupTitle
        {
            get { return (string)GetValue(GroupTitleProperty); }
            set { SetValue(GroupTitleProperty, value); }
        }

        // Using a DependencyProperty as the backing store for GroupTitle.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty GroupTitleProperty =
            DependencyProperty.Register("GroupTitle", typeof(string), typeof(SelectItemsControl), new UIPropertyMetadata(""));

        #endregion

        #region ItemSourceList
        public IEnumerable<ISelectable> ItemSourceList
        {
            get { return (IEnumerable<ISelectable>)GetValue(ItemSourceListProperty); }
            set { SetValue(ItemSourceListProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Items.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemSourceListProperty =
            DependencyProperty.Register("ItemSourceList", typeof(IEnumerable), typeof(SelectItemsControl));


        #endregion

        #region ItemTemplate

        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemTemplateProperty =
            DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(SelectItemsControl),
            new UIPropertyMetadata(default(DataTemplate)));

        #endregion


        #region DescriptionTemplate

        public DataTemplate DescriptionTemplate
        {
            get { return (DataTemplate)GetValue(DescriptionTemplateProperty); }
            set { SetValue(DescriptionTemplateProperty, value); }
        }

        // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DescriptionTemplateProperty =
            DependencyProperty.Register("DescriptionTemplate", typeof(DataTemplate), typeof(SelectItemsControl),
            new UIPropertyMetadata(default(DataTemplate), OnDescriptionTemplateChanged));

        private static void OnDescriptionTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
        }

        #endregion

        #region ItemSelected

        public bool ItemSelected
        {
            get { return (bool)GetValue(ItemSelectedProperty); }
            set { SetValue(ItemSelectedProperty, value); }
        }

        // Using a DependencyProperty as the backing store for EnabledCheck.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemSelectedProperty =
            DependencyProperty.Register("ItemSelectedCheck", typeof(bool), typeof(SelectItemsControl), new UIPropertyMetadata(false));

        #endregion

        #region ItemDescription



        public string ItemDescription
        {
            get { return (string)GetValue(ItemDescriptionProperty); }
            set { SetValue(ItemDescriptionProperty, value); }
        }

        // Using a DependencyProperty as the backing store for ItemDescription.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty ItemDescriptionProperty =
            DependencyProperty.Register("ItemDescription", typeof(string), typeof(SelectItemsControl), new UIPropertyMetadata(""));


        #endregion

        #region SelectAllCheck
        public bool SelectAll
        {
            get { return (bool)GetValue(SelectAllProperty); }
            set { SetValue(SelectAllProperty, value); }
        }

        // Using a DependencyProperty as the backing store for SelectAll.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SelectAllProperty =
            DependencyProperty.Register("SelectAll", typeof(bool), typeof(SelectItemsControl), new UIPropertyMetadata(false));


        #endregion

        #endregion

        #region Private Members

        private TextBlock _partTitle;
        private CheckBox _partEnabledCheck;
        private TextBlock _partGroupTitle;
        private ListView _partItemsListView;
        private CheckBox _partSelectAllCheck;

        #endregion

        /// <summary>
        /// 
        /// </summary>
        static SelectItemsControl()
        {
            DefaultStyleKeyProperty.OverrideMetadata(typeof(SelectItemsControl), 
                new FrameworkPropertyMetadata(typeof(SelectItemsControl)));
        }

        public SelectItemsControl()
        {
            Loaded += OnLoaded;
            Unloaded += OnUnloaded;
        }

        private void OnLoaded(object sender, RoutedEventArgs routedEventArgs)
        {
            if (ItemTemplate == null)
            {
                CreateDefaultItemTemplate();
            }
            PresentationSource presentationSource = PresentationSource.FromVisual((Visual)sender);

            // Subscribe to PresentationSource's ContentRendered event
            // ReSharper disable once PossibleNullReferenceException
            presentationSource.ContentRendered += SelectItemsControl_ContentRendered;
        }

        private void SelectItemsControl_ContentRendered(object sender, EventArgs e)
        {
            // Don't forget to unsubscribe from the event
            ((PresentationSource)sender).ContentRendered -= SelectItemsControl_ContentRendered;
            ListenToSelectedCheckBoxClickEvent(_partItemsListView, true);
        }

        private void OnUnloaded(object sender, RoutedEventArgs e)
        {
            ListenToSelectedCheckBoxClickEvent(_partItemsListView, false);
        }

        private void CreateDefaultItemTemplate()
        {
            DataTemplate template = new DataTemplate { DataType = typeof(ListViewItem) };
            FrameworkElementFactory stackPanelFactory = new FrameworkElementFactory(typeof(StackPanel));
            stackPanelFactory.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal);

            FrameworkElementFactory selected = new FrameworkElementFactory(typeof(CheckBox));
            selected.SetBinding(TextBlock.TextProperty, new Binding("Selected"));
            stackPanelFactory.AppendChild(selected);

            FrameworkElementFactory title = new FrameworkElementFactory(typeof(TextBlock));
            title.SetBinding(TextBlock.TextProperty, new Binding("Description"));
            stackPanelFactory.AppendChild(title);
        }

        public override void OnApplyTemplate()
        {
            base.OnApplyTemplate();

            // Code to get the Template parts as instance member
            _partTitle = GetTemplateChild("PART_Title") as TextBlock;
            _partEnabledCheck = GetTemplateChild("PART_EnabledCheck") as CheckBox;
            _partGroupTitle = GetTemplateChild("PART_GroupTitle") as TextBlock;
            _partItemsListView = GetTemplateChild("PART_Items") as ListView;
            //_partItemSelectedCheck = GetTemplateChild("PART_ItemSelectedCheck") as CheckBox;
            _partSelectAllCheck = GetTemplateChild("PART_SelectAllCheck") as CheckBox;

            if (_partTitle == null || _partEnabledCheck == null || _partGroupTitle == null || _partItemsListView == null ||
                _partSelectAllCheck == null)
            {
                throw new NullReferenceException("Template parts not available");
            }

            // set visibility
            _partEnabledCheck.Visibility = HasEnabledCheck ? Visibility.Visible : Visibility.Collapsed;
            _partEnabledCheck.Click += PartEnabledCheckOnClick;
            _partTitle.Visibility = string.IsNullOrEmpty(_partTitle.Text) ? Visibility.Collapsed : Visibility.Visible;
            _partGroupTitle.Visibility = string.IsNullOrEmpty(_partGroupTitle.Text) ? Visibility.Collapsed : Visibility.Visible;
            _partSelectAllCheck.Click += PartSelectAllCheckOnClick;

        }

        private void PartEnabledCheckOnClick(object sender, RoutedEventArgs routedEventArgs)
        {
            _partItemsListView.IsEnabled = EnabledCheck;
            _partSelectAllCheck.IsEnabled = EnabledCheck;
        }

        private void ListenToSelectedCheckBoxClickEvent(DependencyObject parent, bool set)
        {
            foreach (CheckBox cb in VisualTreeHelpers.FindVisualChildren<CheckBox>(parent))
            {
                BindingExpression binding = cb.GetBindingExpression(CheckBox.IsCheckedProperty);
                // ReSharper disable once PossibleNullReferenceException
                if (binding.ParentBinding.Path.Path == "Selected")
                {
                    if (set)
                        cb.Click += SelectedCheckBox_Click;
                    else
                        cb.Click -= SelectedCheckBox_Click;
                }
            }
            
        }

        private void SelectedCheckBox_Click(object sender, RoutedEventArgs e)
        {
            _partSelectAllCheck.IsChecked = !ItemSourceList.AsQueryable().Any(x => x.Selected == false);

        }

        private void PartSelectAllCheckOnClick(object sender, RoutedEventArgs routedEventArgs)
        {
            foreach (CheckBox cb in VisualTreeHelpers.FindVisualChildren<CheckBox>(_partItemsListView))
            {
                BindingExpression binding = cb.GetBindingExpression(CheckBox.IsCheckedProperty);
                // ReSharper disable once PossibleNullReferenceException
                if (binding.ParentBinding.Path.Path == "Selected")
                {
                    cb.IsChecked = _partSelectAllCheck.IsChecked ?? false;
                }
            }
        }

    }
}

Could someone please post some code which shows how set - create the default template?


Solution

  • This turned out to be simpler than I thought. Because the ItemTemplate is bound to a dependency property I can specify the default template there. That just left the creation of the template. See below.

            #region ItemTemplate
    
            public DataTemplate ItemTemplate
            {
                get { return (DataTemplate)GetValue(ItemTemplateProperty); }
                set { SetValue(ItemTemplateProperty, value); }
            }
    
            // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
            public static readonly DependencyProperty ItemTemplateProperty =
                DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(SelectItemsControl),
                new UIPropertyMetadata(DefaultItemTemplate));
    
            private static DataTemplate DefaultItemTemplate
            {
                get
                {
                    // tried using a MemoryStream - StreamWriter but was getting a
                    // "Root element missing error", would be nice to know why.
                    var sb = new StringBuilder();
    
                    sb.Append("<DataTemplate xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">");
                    sb.Append("<StackPanel Orientation=\"Horizontal\">");
                    sb.Append("<CheckBox Margin=\"2\" IsChecked=\"{Binding Selected}\" />");
                    sb.Append("<TextBlock Margin=\"5,2\" Text=\"{Binding Description}\" VerticalAlignment=\"Center\"/>");
                    sb.Append("</StackPanel>");
                    sb.Append("</DataTemplate>");
    
                    var myByteArray = System.Text.Encoding.UTF8.GetBytes(sb.ToString());
                    var ms = new MemoryStream(myByteArray);
    
                    return (DataTemplate) XamlReader.Load(ms);
                }
            }
    
            #endregion