zoukankan      html  css  js  c++  java
  • [UWP 自定义控件]了解模板化控件(8):ItemsControl

    1. 模仿ItemsControl

    顾名思义,ItemsControl是展示一组数据的控件,它是UWP UI系统中最重要的控件之一,和展示单一数据的ContentControl构成了UWP UI的绝大部分,ComboBox,ListBox,ListView,FlipView,GridView等控件都继承自ItemsControl。曾经有个说法:了解ContentControl和ItemsControl才能算是了解WPF的控件,这一点在UWP中也是一样的。

    以我的经验来说,通过继承ItemsControl来自定义模板化控件十分常见,了解ItemsControl对将来要自定义模板化控件十分有用。但ItemsControl的话题十分庞大,和ContentControl不同,不太适合在这里展开讨论,所以这里就只是稍微讨论核心的思想。

    虽然ItemsControl及其派生类很复杂,但核心功能很简单,所以索性自己实现一次。这次用于讨论的SimpleItemsControl直接继承自Control,简单地模仿ItemsControl实现了它基本的功能,通过这个控件可以一窥ItemsControl的原理。在XAML中使用如下,基本上和ItemsControl一样:

    <StackPanel Margin="20" HorizontalAlignment="Center">
        <local:SimpleItemsControl>
           <ContentPresenter Content="this is ContentPresenter" />
             <Rectangle  Height="50"
                        HorizontalAlignment="Stretch"
                        Fill="Red" />
            <local:ScoreModel />
        </local:SimpleItemsControl>
        
        <local:SimpleItemsControl Margin="0,20,0,0">
            <local:SimpleItemsControl.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Score}" />
                </DataTemplate>
            </local:SimpleItemsControl.ItemTemplate>
            <local:ScoreModel Score="70" />
            <local:ScoreModel Score="80" />
            <local:ScoreModel Score="90" />
            <local:ScoreModel Score="100" />
        </local:SimpleItemsControl>
    </StackPanel>
    
    

    SimpleItemsControl除了没有ItemsSource、ItemsPanelTemplate及虚拟化等功能等功能外,拥有ItemsControl基本的功能。

    1.1 Items属性

    public ICollection<object> Items
    	{
                get;
    	}
    
    

    实现这个控件首要的是提供Items属性,Items在构造函数中实例化成ObservableCollection类型,并且订阅它的CollectionChanged事件。注意:TemplatedControl中的集合属性通常都被可以被实例化成O巴塞尔,以便监视事件。

    var items = new ObservableCollection<object>();
    items.CollectionChanged += OnItemsCollectionChanged;
    Items = items;
    
    

    当然,为了可以在XAML的子节点直接添加元素,别忘了使用ContentPropertyAttribute。

    [ContentProperty(Name = "Items")]
    

    1.2 ItemsPanel

    在ItemsControl中,ControlTemplate包含一个ItemsPresenter,它根据ItemsControl的ItemsPanelTemplate生成一个Panel,并且把Items中各个元素放入这个Panel。

    SimpleItemsControl由于不是继承自ItemsControl,所以直接在ControlTemplate中放一个StackPanel代替。

    _itemsPanel = GetTemplateChild(ItemsPanelPartName) as Panel;
    
    
    <Style TargetType="local:SimpleItemsControl">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:SimpleItemsControl">
                    <StackPanel Background="{TemplateBinding Background}"
                                BorderBrush="{TemplateBinding BorderBrush}"
                                BorderThickness="{TemplateBinding BorderThickness}">
                        <StackPanel x:Name="ItemsPanel" />
                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    
    

    ControlTemplate中只需要一个用于承载Items的ItemsPanel。在这个例子中使用StackPanel。

    1.3 ItemTemplate属性

    接下来需要提供public DataTemplate ItemTemplate { get; set; }属性,它定义了Items中每一项数据如何显示。事实上Items中每一项通常都默认使用ContentControl或ContentPresenter显示(譬如ListBoxItem和ComboxItem),所以ItemTemplate相当于它们的ContentTemplate。熟悉ContentControl的话会更容易理解这个属性。

    1.4 GetContainerForItemOverride

    //
    // 摘要:
    //     创建或标识用于显示给定项的元素。
    //
    // 返回结果:
    //     用于显示给定项的元素。
    protected virtual DependencyObject GetContainerForItemOverride()
    {
        return new ContentPresenter();
    }
    
    

    ItemsControl使用GetContainerForItemOverride函数为Items中每一个item创建它的容器用于在UI上显示,默认是ContentPresenter。对于不是派生自UIElement的Item,它们无法直接在UI上显示,所以Container是必须的。

    1.5 IsItemItsOwnContainerOverride

    //
    // 摘要:
    //     确定指定项是否是为自身的容器,或是否可以作为其自身的容器。
    //
    // 参数:
    //   item:
    //     要检查的项。
    //
    // 返回结果:
    //     如果项是其自己的容器(或可以作为自己的容器),则为 true;否则为 false。
    protected virtual System.Boolean IsItemItsOwnContainerOverride(System.Object item)
    {
        return item is ContentPresenter;
    }
    
    

    对于Items中的每一个item,ItemsControl在为它创建容器前都用这个方法检查它是不是就是容器本身。譬如这段XAML:

    <local:SimpleItemsControl>
        <ContentPresenter Content="this is ContentPresenter" />
        <Rectangle  Height="50"
                    Width="200"
                    Fill="Red" />
        <local:ScoreModel />
    </local:SimpleItemsControl>
    
    

    在这段XAML中,ContentPresenter本身就是容器,所以它将直接被放到ItemsPanel中;Rectangle 不是容器,需要创建一个ContentPresenter,将Rectangle 设置为这个ContentPresenter的Content再放到ItemsPanel中。

    1.6 PrepareContainerForItemOverride

    //
    // 摘要:
    //     准备指定元素以显示指定项。
    //
    // 参数:
    //   element:
    //     用于显示指定项的元素。
    //
    //   item:
    //     要显示的项。
    protected virtual void PrepareContainerForItemOverride(DependencyObject element, System.Object item)
    {
        ContentControl contentControl;
        ContentPresenter contentPresenter;
    
        if ((contentControl = element as ContentControl) != null)
        {
            contentControl.Content = item;
            contentControl.ContentTemplate = ItemTemplate;
        }
        else if ((contentPresenter = element as ContentPresenter) != null)
        {
            contentPresenter.Content = item;
            contentPresenter.ContentTemplate = ItemTemplate;
        }
    }
    
    

    这个方法在Item被呈现到UI前调用,目标是设定ContainerForItem中的某些值,譬如Content及ContentTemplate。其中参数element即之前创建的ContainerForItem(也有可能是Item自己)。在调用这个函数后ContainerForItem将被放到ItemsPanel中。

    1.7 UpdateView

    private void UpdateView()
    {
        if (_itemsPanel == null)
            return;
    
        _itemsPanel.Children.Clear();
        foreach (var item in Items)
        {
            DependencyObject container;
            if (IsItemItsOwnContainerOverride(item))
            {
                container = item as DependencyObject;
            }
            else
            {
                container = GetContainerForItemOverride();
                PrepareContainerForItemOverride(container, item);
            }
           
            if (container is UIElement)
                _itemsPanel.Children.Add(container as UIElement);
        }
    }
    
    

    这个函数在OnItemsCollectionChanged或OnApplyTemplate后调用,简单地将ItemsPanel.Children清空,然后将所有Item创建容器(或者不创建)然后放进ItemsPanel。实际上ItemsControl的逻辑要复杂很多,这里只是个极端简化的版本。

    到这一步一个简单的ItemsControl就完成了,总共只有100多行代码。

    看到这里可能会有个疑惑,GetContainerForItemOverride、IsItemItsOwnContainerOverride、PrepareContainerForItemOverride三个函数明明做的是同一件事(为Item创建Container),为什么要将它们分开?这是因为ItemsControl支持使用UI虚拟化技术。

    假设Items中包含一万个项,为这一万个项创建容器并放到ItemsPanel上,将会造成巨大的内存消耗。而且拖动ItemsControl的滚动条时由于要将所有一万个容器同时移动,对CPU造成很大的负担。UI虚拟化就是为了解决这两个问题。通常一个ItemsControl能同时显示的Item最多几十个,ItemsControl就只是创建几十个容器,在拖动滚动条时回收移出可视范围的容器,更改容器的内容(因为容器通常是ContentControl,所以就是更改ContentControl.Content),再重新放到可视范围里面。为了实现这个技术,Item和它的Container就不能是一一对应的,所以才会把上述的三个函数分离。

    注意: UWP中ItemsControl默认没有启用UI虚拟化,但它的派生类有。

    1.8 完整的代码

    [TemplatePart(Name = ItemsPanelPartName, Type = typeof(Panel))]
    [ContentProperty(Name = "Items")]
    public class SimpleItemsControl : Control
    {
        private const string ItemsPanelPartName = "ItemsPanel";
        public SimpleItemsControl()
        {
            this.DefaultStyleKey = typeof(SimpleItemsControl);
            var items = new ObservableCollection<object>();
            items.CollectionChanged += OnItemsCollectionChanged;
            Items = items;
        }
    
        /// <summary>
        /// 获取或设置ItemTemplate的值
        /// </summary>  
        public DataTemplate ItemTemplate
        {
            get { return (DataTemplate)GetValue(ItemTemplateProperty); }
            set { SetValue(ItemTemplateProperty, value); }
        }
    
        /// <summary>
        /// 标识 ItemTemplate 依赖属性。
        /// </summary>
        public static readonly DependencyProperty ItemTemplateProperty =
            DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(SimpleItemsControl), new PropertyMetadata(null, OnItemTemplateChanged));
    
        private static void OnItemTemplateChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            SimpleItemsControl target = obj as SimpleItemsControl;
            DataTemplate oldValue = (DataTemplate)args.OldValue;
            DataTemplate newValue = (DataTemplate)args.NewValue;
            if (oldValue != newValue)
                target.OnItemTemplateChanged(oldValue, newValue);
        }
    
        protected virtual void OnItemTemplateChanged(DataTemplate oldValue, DataTemplate newValue)
        {
            UpdateView();
        }
    
        public ICollection<object> Items
        {
            get;
        }
    
        private Panel _itemsPanel;
    
        protected override void OnApplyTemplate()
        {
            base.OnApplyTemplate();
            _itemsPanel = GetTemplateChild(ItemsPanelPartName) as Panel;
            UpdateView();
        }
    
        private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            UpdateView();
        }
    
    
        //
        // 摘要:
        //     创建或标识用于显示给定项的元素。
        //
        // 返回结果:
        //     用于显示给定项的元素。
        protected virtual DependencyObject GetContainerForItemOverride()
        {
            return new ContentPresenter();
        }
    
    
        //
        // 摘要:
        //     确定指定项是否是为自身的容器,或是否可以作为其自身的容器。
        //
        // 参数:
        //   item:
        //     要检查的项。
        //
        // 返回结果:
        //     如果项是其自己的容器(或可以作为自己的容器),则为 true;否则为 false。
        protected virtual System.Boolean IsItemItsOwnContainerOverride(System.Object item)
        {
            return item is ContentPresenter;
        }
    
        //
        // 摘要:
        //     准备指定元素以显示指定项。
        //
        // 参数:
        //   element:
        //     用于显示指定项的元素。
        //
        //   item:
        //     要显示的项。
        protected virtual void PrepareContainerForItemOverride(DependencyObject element, System.Object item)
        {
            ContentControl contentControl;
            ContentPresenter contentPresenter;
    
            if ((contentControl = element as ContentControl) != null)
            {
                contentControl.Content = item;
                contentControl.ContentTemplate = ItemTemplate;
            }
            else if ((contentPresenter = element as ContentPresenter) != null)
            {
                contentPresenter.Content = item;
                contentPresenter.ContentTemplate = ItemTemplate;
            }
        }
    
        private void UpdateView()
        {
            if (_itemsPanel == null)
                return;
    
            _itemsPanel.Children.Clear();
            foreach (var item in Items)
            {
                DependencyObject container;
                if (IsItemItsOwnContainerOverride(item))
                {
                    container = item as DependencyObject;
                }
                else
                {
                    container = GetContainerForItemOverride();
                    PrepareContainerForItemOverride(container, item);
                }
                   
                if (container is UIElement)
                    _itemsPanel.Children.Add(container as UIElement);
            }
        }
    }
    
    

    2. 扩展ItemsControl

    了解过ItemsControl的原理,或通过继承ItemsControl自定义控件就很简单了。譬如要实现这个功能:一个事件列表,自动为事件添加上触发的时间。效果如下:

    通过重载GetContainerForItemOverride、IsItemItsOwnContainerOverride、PrepareContainerForItemOverride这三个函数,很简单就能实现这个需求:

    public class EventListView : ListView
    {
        public EventListView()
        {
            _items = new Dictionary<object, DateTime>();
        }
    
        private Dictionary<object, DateTime> _items;
    
        protected override DependencyObject GetContainerForItemOverride()
        {
            return new HeaderedContentControl();
        }
    
        protected override bool IsItemItsOwnContainerOverride(object item)
        {
            return item is HeaderedContentControl;
        }
    
        protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
        {
            base.PrepareContainerForItemOverride(element, item);
            var control = element as HeaderedContentControl;
            control.Content = item;
            if (_items.ContainsKey(item))
            {
                var time = _items[item];
                control.Header = time.ToString("HH:mm:ss")+": ";
            }
        }
    
        protected override void OnItemsChanged(object e)
        {
            base.OnItemsChanged(e);
            foreach (var item in Items)
            {
                if (_items.ContainsKey(item) == false)
                    _items.Add(item, DateTime.Now);
            }
        }
    }
    
    
    public sealed class EventListViewItem : ListViewItem
    {
        public EventListViewItem()
        {
            this.DefaultStyleKey = typeof(EventListViewItem);
        }
    
        public object Header
        {
            get { return (object)GetValue(HeaderProperty); }
            set { SetValue(HeaderProperty, value); }
        }
    
        // Using a DependencyProperty as the backing store for Header.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HeaderProperty =
            DependencyProperty.Register("Header", typeof(object), typeof(EventListViewItem), new PropertyMetadata(null));
    
    
    }
    
    

    3. 集合类型属性

    在XAML中使用集合类型属性,通常不会这样:

    <ItemsControl>
        <ItemsControl.Items>
            <ItemCollection>
                <local:ScoreModel Score="70" />
                <local:ScoreModel Score="80" />
                <local:ScoreModel Score="90" />
                <local:ScoreModel Score="100" />
            </ItemCollection>
        </ItemsControl.Items>
    </ItemsControl>
    
    

    而是这样:

    <ItemsControl>
        <ItemsControl.Items>
            <local:ScoreModel Score="70" />
            <local:ScoreModel Score="80" />
            <local:ScoreModel Score="90" />
            <local:ScoreModel Score="100" />
        </ItemsControl.Items>
    </ItemsControl>
    
    

    因为集合类型属性通常定义为只读的,不必也不可以对它赋值,只可以向它添加内容。

    控件中的集合属性一般遵循以下做法:

    3.1 只读属性

    public IList<HubSection> Sections { get; }
    

    这是Hub的Section属性,模板化控件中的集合类型属性基本都定义成这样的CLR属性。

    3.2 监视更改通知

    如果需要监视集合项更改,可以将属性定义为继承INotifyCollectionChanged 自的集合类型,譬如 ObservableCollection。

    3.3 不使用依赖属性

    因为集合属性通常不会使用动画,或者通过Style中的Setter赋值,而且依赖属性标识符是静态的,集合属性的初始值有可能引起单例的问题。集合属性通常在构造函数中初始化。

    3.4 绑定到集合属性

    通常不会绑定到集合属性,更常见的做法是如ItemsControl那样,绑定到ItemsSource。

  • 相关阅读:
    name mangling
    Haskell: What is Weak Head Normal Form
    取模运算和取余运算的区别
    a common method to rotate the image
    代码静态分析工具
    LeeCode-Single Number III
    七夕这天
    mysql TO_DAYS()
    (转)剖析Linux文件编码的查看及修改
    docker
  • 原文地址:https://www.cnblogs.com/dino623/p/TemplatedControlAndItemsControl.html
Copyright © 2011-2022 走看看