[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。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM