[WPF自定義控制項庫]瞭解如何自定義ItemsControl

来源:https://www.cnblogs.com/dino623/archive/2019/05/20/Custom-ItemsControl.html
-Advertisement-
Play Games

1. 前言 對WPF來說ContentControl和 "ItemsControl" 是最重要的兩個控制項。 顧名思義,ItemsControl表示可用於呈現一組Item的控制項。大部分時候我們並不需要自定義ItemsControl,因為WPF提供了一大堆ItemsControl的派生類:Headere ...


1. 前言

對WPF來說ContentControl和ItemsControl是最重要的兩個控制項。

顧名思義,ItemsControl表示可用於呈現一組Item的控制項。大部分時候我們並不需要自定義ItemsControl,因為WPF提供了一大堆ItemsControl的派生類:HeaderedItemsControl、TreeView、Menu、StatusBar、ListBox、ListView、ComboBox;而且配合Style或DataTemplate足以完成大部分的定製化工作,可以說ItemsControl是XAML系統靈活性的最佳代表。不過,既然它是最常用的控制項,那麼掌握一些它的原理對所有WPF開發者都有好處。

我以前寫過一篇文章介紹如何模仿ItemsControl,並且博客園也已經很多文章深入介紹ItemsControl的原理,所以這篇文章只介紹簡單的自定義ItemsControl知識,通過重寫GetContainerForItemOverride和IsItemItsOwnContainerOverride、PrepareContainerForItemOverride函數並使用ItemContainerGenerator等自定義一個簡單的IItemsControl控制項。

2. 介紹作為例子的Repeater

作為教學我創建了一個繼承自ItemsControl的控制項Repeater(雖然簡單,用來展示資料的話好像還真的有點用)。它的基本用法如下:

<local:Repeater>
    <local:RepeaterItem Content="1234999"
                        Label="Product ID" />
    <local:RepeaterItem Content="Power Projector 4713"
                        Label="IGNORE" />
    <local:RepeaterItem Content="Projector (PR)"
                        Label="Category" />
    <local:RepeaterItem Content="A very powerful projector with special features for Internet usability, USB"
                        Label="Description" />
</local:Repeater>

也可以不直接使用Items,而是綁定ItemsSource並指定DisplayMemberPath和LabelMemberPath。

public class Product
{
    public string Key { get; set; }

    public string Value { get; set; }

    public static IEnumerable<Product> Products
    {
        get
        {
            return new List<Product>
            {
                new Product{Key="Product ID",Value="1234999" },
                new Product{Key="IGNORE",Value="Power Projector 4713" },
                new Product{Key="Category",Value="Projector (PR)" },
                new Product{Key="Description",Value="A very powerful projector with special features for Internet usability, USB" },
                new Product{Key="Price",Value="856.49 EUR" },
            };

        }
    }
}
<local:Repeater ItemsSource="{x:Static local:Product.Products}"
                DisplayMemberPath="Value"
                LabelMemberPath="Key"/>

運行結果如下圖:

3. 實現

確定好需要實現的ItemsControl後,通常我大致會使用三步完成這個ItemsControl:

  1. 定義ItemContainer
  2. 關聯ItemContainer和ItemsControl
  3. 實現ItemsControl的邏輯

3.1 定義ItemContainer

派生自ItemsControl的控制項通常都會有匹配的子元素控制項,如ListBox對應ListBoxItem,ComboBox對應ComboBoxItem。如果ItemsControl的Items內容不是對應的子元素控制項,ItemsControl會創建對應的子元素控制項作為容器再把Item放進去。

<ListBox>
    <system:String>Item1</system:String>
    <system:String>Item2</system:String>
</ListBox>

例如這段XAML中,Item1和Item2是ListBox的LogicalChildren,而它們會被ListBox封裝到ListBoxItem,ListBoxItem才是ListBox的VisualChildren。在這個例子中,ListBoxItem可以稱作ItemContainer

ItemsControl派生類的ItemContainer控制項要使用父元素名稱做首碼、-Item做尾碼,例如ComboBox的子元素ComboBoxItem,這是WPF約定俗成的做法(不過也有TabControl和TabItem這種例外)。Repeater也派生自ItemsControl,Repeatertem即為Repeater的ItemContainer控制項。

public RepeaterItem()
{
    DefaultStyleKey = typeof(RepeaterItem);
}

public object Label
{
    get => GetValue(LabelProperty);
    set => SetValue(LabelProperty, value);
}

public DataTemplate LabelTemplate
{
    get => (DataTemplate)GetValue(LabelTemplateProperty);
    set => SetValue(LabelTemplateProperty, value);
}
<Style TargetType="local:RepeaterItem">
    <Setter Property="Padding"
            Value="8" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:RepeaterItem">
                <Border BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        Background="{TemplateBinding Background}">
                    <StackPanel Margin="{TemplateBinding Padding}">
                        <ContentPresenter Content="{TemplateBinding Label}"
                                          ContentTemplate="{TemplateBinding LabelTemplate}"
                                          VerticalAlignment="Center"
                                          TextBlock.Foreground="#FF777777" />
                        <ContentPresenter x:Name="ContentPresenter" />
                    </StackPanel>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

上面是RepeaterItem的代碼和DefaultStyle。RepeaterItem繼承ContentControl並提供Label、LabelTemplate。DefaultStyle的做法參考ContentControl。

3.2 關聯ItemContainer和ItemsControl

<Style TargetType="{x:Type local:Repeater}">
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility"
            Value="Auto" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:Repeater}">
                <Border BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        Background="{TemplateBinding Background}">
                    <ScrollViewer Padding="{TemplateBinding Padding}">
                        <ItemsPresenter />
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

如上面XAML所示,Repeater的ControlTemplate中需要提供一個ItemsPresenter,用於指定ItemsControl中的各Item擺放的位置。

[StyleTypedProperty(Property = "ItemContainerStyle", StyleTargetType = typeof(RepeaterItem))]
public class Repeater : ItemsControl
{
    public Repeater()
    {
        DefaultStyleKey = typeof(Repeater);
    }

    protected override bool IsItemItsOwnContainerOverride(object item)
    {
        return item is RepeaterItem;
    }

    protected override DependencyObject GetContainerForItemOverride()
    {
        var item = new RepeaterItem();
        return item;
    }
}

Repeater的基本代碼如上所示。要將Repeater和RepeaterItem關聯起來,除了使用約定俗成的命名方式告訴用戶,還需要使用下麵兩步:

重寫 GetContainerForItemOverride
protected virtual DependencyObject GetContainerForItemOverride () 用於返回Item的Container。Repeater返回的是RepeaterItem。

重寫 IsItemItsOwnContainer
protected virtual bool IsItemItsOwnContainerOverride (object item),確定Item是否是(或者是否可以作為)其自己的Container。在Repeater中,只有RepeaterItem返回True,即如果Item的類型不是RepeaterItem,就將它作使用RepeaterItem包裝起來。

完成上面幾步後,為Repeater設置ItemsSource的話Repeater將會創建對應的RepeaterItem並添加到自己的VisualTree下麵。

使用 StyleTypedPropertyAttribute

最後可以在Repeater上添加StyleTypedPropertyAttribute,指定ItemContainerStyle的類型為RepeaterItem。添加這個Attribute後在Blend中選擇“編輯生成項目的容器(ItemContainerStyle)”就會預設使用RepeaterItem的樣式。

3.3 實現ItemsControl的邏輯

public string LabelMemberPath
{
    get => (string)GetValue(LabelMemberPathProperty);
    set => SetValue(LabelMemberPathProperty, value);
}

/*LabelMemberPathProperty Code...*/

protected virtual void OnLabelMemberPathChanged(string oldValue, string newValue)
{
    // refresh the label member template.
    _labelMemberTemplate = null;
    var newTemplate = LabelMemberPath;

    int count = Items.Count;
    for (int i = 0; i < count; i++)
    {
        if (ItemContainerGenerator.ContainerFromIndex(i) is RepeaterItem RepeaterItem)
            PrepareRepeaterItem(RepeaterItem, Items[i]);
    }
}

private DataTemplate _labelMemberTemplate;

private DataTemplate LabelMemberTemplate
{
    get
    {
        if (_labelMemberTemplate == null)
        {
            _labelMemberTemplate = (DataTemplate)XamlReader.Parse(@"
            <DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""
                        xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"">
                    <TextBlock Text=""{Binding " + LabelMemberPath + @"}"" VerticalAlignment=""Center""/>
            </DataTemplate>");
        }

        return _labelMemberTemplate;
    }
}

protected override void PrepareContainerForItemOverride(DependencyObject element, object item)
{
    base.PrepareContainerForItemOverride(element, item);

    if (element is RepeaterItem RepeaterItem )
    {
        PrepareRepeaterItem(RepeaterItem,item);
    }
}

private void PrepareRepeaterItem(RepeaterItem RepeaterItem, object item)
{
    if (RepeaterItem == item)
        return;

    RepeaterItem.LabelTemplate = LabelMemberTemplate;
    RepeaterItem.Label = item;
}

Repeater本身沒什麼複雜的邏輯,只是模仿DisplayMemberPath添加了LabelMemberPathLabelMemberTemplate屬性,並把這個屬性和RepeaterItem的Label和'LabelTemplate'屬性關聯起來,上面的代碼即用於實現這個功能。

LabelMemberPath和LabelMemberTemplate
Repeater動態地創建一個內容為TextBlock的DataTemplate,這個TextBlock的Text綁定到LabelMemberPath

XamlReader相關的技術我在如何使用代碼創建DataTemplate這篇文章里講解了。

ItemContainerGenerator.ContainerFromIndex
ItemContainerGenerator.ContainerFromIndex(Int32)返回ItemsControl中指定索引處的Item,當Repeater的LabelMemberPath改變時,Repeater首先強制更新了LabelMemberTemplate,然後用ItemContainerGenerator.ContainerFromIndex找到所有的RepeaterItem並更新它們的Label和LabelTemplate。

PrepareContainerForItemOverride
protected virtual void PrepareContainerForItemOverride (DependencyObject element, object item) 用於在RepeaterItem添加到UI前為其做些準備工作,其實也就是為RepeaterItem設置LabelLabelTemplate而已。

4. 結語

實際上WPF的ItemsControl很強大也很複雜,源碼很長,對初學者來說我推薦參考Moonlight中的實現(Moonlight, an open source implementation of Silverlight for Unix systems),上面LabelMemberTemplate的實現就是抄Moonlight的。Silverlight是WPF的簡化版,Moonlight則是很久沒維護的Silverlight的簡陋版,這使得Moonlight反而成了很優秀的WPF教學材料。

當然,也可以參考Silverlight的實現,使用JustDecompile可以輕鬆獲取Silverlight的源碼,這也是很好的學習材料。不過ItemsControl的實現比Moonlight多了將近一倍的代碼。

5. 參考

ItemsControl Class (System.Windows.Controls) Microsoft Docs
moon_ItemsControl.cs at master
ItemContainer Control Pattern - Windows applications _ Microsoft Docs


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 現象: 報錯:Exception in thread "main" com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure 解決辦法: 更換 mysql-connector-java-8.0. ...
  • 一、單體架構的問題 微服務為什麼會出現?在學習Springboot的時候知道Springboot極大的簡化了我們的開發,我們可以快速的進行業務開發,Springboot單體應用在項目的開發初期能夠滿足我們需求,這種單體架構優點非常的明顯: 容易測試:本地就可以起完整的系統,不需要外部依賴。 容易開發 ...
  • 在Kinect for windows SDK2.0中,獲取並處理數據源介面步驟如下: Sensor -> Source -> Reader -> Frame -> Data (一)SensorIKinectSensor *pSensor=nullptr; //定義類指針GetDefaultKine ...
  • UDP協議: 1、python中基於udp協議的客戶端與服務端通信簡單過程實現 2、udp協議的一些特點(與tcp協議的比較) 3、利用socketserver模塊實現udp傳輸協議的併發通信 一、UDP協議:OSI七層協議中的傳輸協議的一種(另外一種tcp協議),他們都是一種埠協議 與TCP協議 ...
  • List集合有序、元素可重覆。以元素的添加順序作為集合的排列順序,用下標索引集合中的元素。 List因為使用下標索引元素,所以元素可重覆。Set使用元素本身來索引,所以元素不能重覆。 List的繼承關係: List繼承了Collection的所有方法,也有自身的一些方法(下標操作): void ad ...
  • 1.什麼是列表? 列表是由一組按特定順序排列的元素組成。 2.如何表示? 在Python中用方括弧([ ])來表示列表。慄子如下: 控制台列印如下: 沒錯,它將列印列表的內部表示,包括方括弧。 3.如何去訪問列表元素? console(控制台): 所以說訪問列表的元素,可以在列表名後加方括弧,方括弧 ...
  • hasNextInt() :判斷是否還有下一個輸入項,其中Xxx可以是Int,Double等。如果需要判斷是否包含下一個字元串,則可以省略Xxx nextInt(): 獲取下一個輸入項。Xxx的含義和上個方法中的Xxx相同,預設情況下,Scanner使用空格,回車等作為分隔符 public stat ...
  • 1. 是否保證線程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保證線程安全; 2. 底層數據結構: Arraylist 底層使用的是Object數組;LinkedList 底層使用的是雙向鏈表數據結構(JDK1.6之前為迴圈鏈表,JDK1.7取消了迴圈。註意雙向鏈表和 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...