UWP下控件开发06 - 完善虚拟化方案

本篇文章的代码示例均为 C++/CX,很容易转换为 C#。

唔,最近没怎么开发 UWP 了,所以 UWP 控件开发与 UWP 风向标系列都一直没有什么更新,之前和朋友研究了很久的响应式与虚拟化的兼容方案,最后讨论了很久都没有什么特别好的解决方案,由于 UWP 里面将虚拟化的接口内部化了,无法直接继承,WPF 原有的虚拟化方案也不能直接在 UWP 上套用。

要想比较完美的实现虚拟化,还是必须从头开始实现,本篇文章将会分为两个部分如何创建一个瀑布流支持虚拟化的面板,第一部分将会着重描述如何为通用的虚拟化面板做准备工作,第二个部分则会着重描述如何在瀑布流面板中使用虚拟化。

图1:开启虚拟化时,列表中有 101 个项目,但是仅有 16 个项目在可视化树中。

在此之前如果你对于虚拟化还不够清楚可以阅读 UWP下控件开发04 - 虚拟化面板实现 ,该文章简单的描述了虚拟化在应用中的作用。

在上面这篇文章中,我们的虚拟化不够完善,在可视化树中仍然有很多项目在面板中,但是被虚拟化的项目是没有内容的空容器,但是过多的空容器元素在切换页面时仍然会导致 UI 卡顿。

VirtualizingPanel 的成员声明

首先是创建继承自普通的面板的虚拟化面板基类

using namespace Platform;  
using namespace Platform::Collections;  
using namespace Windows::Foundation;  
using namespace Windows::Foundation::Collections;  
using namespace Windows::UI::Xaml;  
namespace WinCon = ::Windows::UI::Xaml::Controls;

namespace Marduk  
{
    namespace Controls
    {
        [Windows::UI::Xaml::Markup::ContentPropertyAttribute(Name = "Items")]
        public ref class VirtualizingPanel : public ::WinCon::Panel
        {
        internal:
            VirtualizingPanel();
        }
    }
}

采用内部构造函数的原因是因为 Windows Runtime 组件要求所有的公开类型都为 sealed 或者没有公开的构造方法,也是由于这个缘故,我们在 UWP 中无法继承系统自带的 VirtualizingPanel。

接下来是关于虚拟化面板呈现相关的依赖属性,例如可绑定的 ItemSource,ItemTemplate,等等其他的。

public ref class VirtualizingPanel :  
    public ::WinCon::Panel
{
    RegisterDependencyProperty(Windows::UI::Xaml::Style^, _itemContainerStyleProperty, ItemContainerStyleProperty, ItemContainerStyle);
    RegisterDependencyProperty(WinCon::StyleSelector^, _itemContainerStyleSelectorProperty, ItemContainerStyleSelectorProperty, ItemContainerStyleSelector);
    RegisterDependencyProperty(DataTemplate^, _itemTemplateProperty, ItemTemplateProperty, ItemTemplate);
    RegisterDependencyProperty(WinCon::DataTemplateSelector^, _itemTemplateSelectorProperty, ItemTemplateSelectorProperty, ItemTemplateSelector);
    RegisterDependencyProperty(Platform::Object^, _itemSourceProperty, ItemSourceProperty, ItemSource);

protected:  
    virtual void RegisterDependencyProperties();

private:  
    static void OnItemSourceChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e);
    static void OnItemTemplateChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e);
    static void OnItemTemplateSelectorChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e);
    static void OnItemContainerStyleChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e);
    static void OnItemContainerStyleSelectorChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e);
}

这些代码声明了一些我们需要用到的依赖属性,与其的改变事件 Handler, RegisterDependencyProperties 方法则是用于在第一次使用的时候注册依赖属性。

RegisterDependencyProperty 是我自定义的一个宏,用于注册类的依赖属性,其具体的实现如下,包含了一些对于 C# 开发者比较实用并且熟悉的宏:

#define typeof(x) Windows::UI::Xaml::Interop::TypeName(x::typeid)
#define nameof(x) #x
#define RegisterProperty(type, field, propertyName)    property type propertyName \
                                                        { \
                                                            type get() \
                                                            { \
                                                                return field; \
                                                            } \
                                                            void set(type value) \
                                                            { \
                                                                field = value; \
                                                            } \
                                                        } 

#define RegisterReadOnlyProperty(type, field, propertyName)    property type propertyName \
                                                                { \
                                                                    type get() \
                                                                    { \
                                                                        return field; \
                                                                    } \
                                                                } 
#define RegisterDependencyProperty(type, fieldName, dependencyPropertyName, propertyName)   public: \
                                                                                                static RegisterReadOnlyProperty(Windows::UI::Xaml::DependencyProperty^, fieldName, dependencyPropertyName) \
                                                                                                property type propertyName \
                                                                                                { \
                                                                                                    type get() \
                                                                                                    { \
                                                                                                        return safe_cast<type>(this->GetValue(dependencyPropertyName)); \
                                                                                                    } \
                                                                                                    void set(type value) \
                                                                                                    { \
                                                                                                        this->SetValue(dependencyPropertyName, value); \
                                                                                                    } \
                                                                                                } \
                                                                                            private: \
                                                                                                static Windows::UI::Xaml::DependencyProperty^ fieldName

typeof 宏用于简单获取到类型的 TypeName,就像 C# 那样。

nameof 宏用于将属性,字段,等变成字符串。

RegisterProperty 宏用于注册一个可读可写的属性。

RegisterReadOnlyProperty 宏用于注册只读属性。

RegisterDependencyProperty 宏用于注册依赖属性。

接下来是关于子项容器生成,回收的方法,我们在这里采用了基本类似于 ItemsControl 的方法。

public ref class VirtualizingPanel :  
    public ::WinCon::Panel
{
protected:  
    virtual bool IsItemItsOwnContainerOverride(Platform::Object^ obj);
    virtual WinCon::ContentControl^ GetContainerForItemOverride();
    virtual void ClearContainerForItemOverride(WinCon::ContentControl^ container, Object^ item);
    virtual void PrepareContainerForItemOverride(WinCon::ContentControl^ container, Object^ item);
    virtual void OnItemContainerStyleChanged(Windows::UI::Xaml::Style^ newStyle, Windows::UI::Xaml::Style^ oldStyle);
    virtual void OnItemContainerStyleSelectorChanged(WinCon::StyleSelector^ newStyleSelector, WinCon::StyleSelector^ oldStyleSelector);
    virtual void OnItemTemplateChanged(DataTemplate^ newTemplate, DataTemplate^ oldTemplate);
    virtual void OnItemTemplateSelectorChanged(WinCon::DataTemplateSelector^ newTemplateSelector, WinCon::DataTemplateSelector^ oldTemplateSelector);
    virtual void OnItemsChanged(IObservableVector<Platform::Object^>^ source, IVectorChangedEventArgs^ e);
    virtual void OnItemSourceChanged(Platform::Object^ newItems, Platform::Object^ oldItems);}
private:  
    void ApplyItemContainerStyle(WinCon::ContentControl^ container, Platform::Object^ item);
    void ApplyItemTemplate(WinCon::ContentControl^ container, Platform::Object^ item);

IsItemItsOwnContainerOverride 方法用于判断子项是否已经可以作为容器,而不需要重新生成。

GetContainerForItemOverride 方法用于获取一个新的子项容器。

ClearContainerForItemOverride 方法在回收容器的时候被调用,用于清理容器,比如 Template 与 Style。

PrepareContainerForItemOverride 方法在容器准备承载子项时被调用,用于为子项设定容器的某些属性,比如 Template 与 Style。

ApplyItemContainerStyle 方法是将设定的样式应用到容器上。

ApplyItemTemplate 方法是将设定的模板应用到容器上。

其他的则是一些用于子类的重写方法。

接下来是关于虚拟化与实例化的一些方法,与需要用到的字段

public ref class VirtualizingPanel :  
    public ::WinCon::Panel
{
protected:  
    RegisterReadOnlyProperty(IVector<WinCon::ContentControl^>^, _recycledContainers, RecycledContainers);
    RegisterReadOnlyProperty(IVector<Platform::Object^>^, _items, Items);
    void RecycleItem(Platform::Object^ item);
    WinCon::ContentControl^ RealizeItem(Platform::Object^ item);
    void RecycleAllItem();

private:  
   Vector<WinCon::ContentControl^>^ _recycledContainers;
   Vector<Platform::Object^>^ _items;

RecycledContainers 属性是一个已经被回收,并且可以被复用的子项容器的列表,当有容器在该列表中时,实例化新项目时会优先选用列表中的容器,而不是重新生成。

Items 属性是子项列表的缓存列表,绑定的 ItemSource 依赖属性的变更也会反应到该列表中,并且易于子类访问。

RecycleItem 方法是回收一个子项的容器。

RealizeItem 方法是实例化一个子项,使其在可视化树中被呈现。

RecycleAllItem 方法能回收所有的子项,一般用于列表被清空。

最后则是关于子项索引,子项对象,包含子项的容器之间的转换的方法

public ref class VirtualizingPanel :  
    public ::WinCon::Panel
{
protected:  
    WinCon::ContentControl^ GetContainerFormItem(Platform::Object^ item);
    WinCon::ContentControl^ GetContainerFormIndex(int index);
    Platform::Object^ GetItemFormContainer(WinCon::ContentControl^ container);
    Platform::Object^ GetItemFormIndex(int index);

private:  
   UnorderedMap<Platform::Object^, WinCon::ContentControl^,HashObject>^ _itemContainerMap;

方法大致看名字就能懂, _itemContainerMap 字段则是一个字典,能够保存子项到容器的映射, HashObject 则能够获取 Object 对象希尔值用于构建 UnorderedMap。

完整的 VirtualizingPanel.h 文件

大致的定义到这里就完成了,下面是完整的 VirtualizingPanel.h 文件

#pragma once
using namespace Platform;  
using namespace Platform::Collections;  
using namespace Windows::Foundation;  
using namespace Windows::Foundation::Collections;  
using namespace Windows::UI::Xaml;  
namespace WinCon = ::Windows::UI::Xaml::Controls;

namespace MoePic  
{
    namespace Controls
    {
        struct HashObject
        {
            size_t operator()(Platform::Object^ obj) const
            {
                return obj->GetHashCode();
            }
        };

        [Windows::UI::Xaml::Markup::ContentPropertyAttribute(Name = "Items")]
        public ref class VirtualizingPanel :
            public ::WinCon::Panel
        {
            RegisterDependencyProperty(Windows::UI::Xaml::Style^, _itemContainerStyleProperty, ItemContainerStyleProperty, ItemContainerStyle);
            RegisterDependencyProperty(WinCon::StyleSelector^, _itemContainerStyleSelectorProperty, ItemContainerStyleSelectorProperty, ItemContainerStyleSelector);
            RegisterDependencyProperty(DataTemplate^, _itemTemplateProperty, ItemTemplateProperty, ItemTemplate);
            RegisterDependencyProperty(WinCon::DataTemplateSelector^, _itemTemplateSelectorProperty, ItemTemplateSelectorProperty, ItemTemplateSelector);
            RegisterDependencyProperty(Platform::Object^, _itemSourceProperty, ItemSourceProperty, ItemSource);

        public:

        internal:
            VirtualizingPanel();

        protected:
            RegisterReadOnlyProperty(IVector<WinCon::ContentControl^>^, _recycledContainers, RecycledContainers);
            RegisterReadOnlyProperty(IVector<Platform::Object^>^, _items, Items);

            virtual void RegisterDependencyProperties();
            virtual bool IsItemItsOwnContainerOverride(Platform::Object^ obj);
            virtual WinCon::ContentControl^ GetContainerForItemOverride();
            virtual void ClearContainerForItemOverride(WinCon::ContentControl^ container, Object^ item);
            virtual void PrepareContainerForItemOverride(WinCon::ContentControl^ container, Object^ item);
            virtual void OnItemContainerStyleChanged(Windows::UI::Xaml::Style^ newStyle, Windows::UI::Xaml::Style^ oldStyle);
            virtual void OnItemContainerStyleSelectorChanged(WinCon::StyleSelector^ newStyleSelector, WinCon::StyleSelector^ oldStyleSelector);
            virtual void OnItemTemplateChanged(DataTemplate^ newTemplate, DataTemplate^ oldTemplate);
            virtual void OnItemTemplateSelectorChanged(WinCon::DataTemplateSelector^ newTemplateSelector, WinCon::DataTemplateSelector^ oldTemplateSelector);
            virtual void OnItemsChanged(IObservableVector<Platform::Object^>^ source, IVectorChangedEventArgs^ e);
            virtual void OnItemSourceChanged(Platform::Object^ newItems, Platform::Object^ oldItems);

            void RecycleItem(Platform::Object^ item);
            WinCon::ContentControl^ RealizeItem(Platform::Object^ item);
            void RecycleAllItem();

            WinCon::ContentControl^ GetContainerFormItem(Platform::Object^ item);
            WinCon::ContentControl^ GetContainerFormIndex(int index);
            Platform::Object^ GetItemFormContainer(WinCon::ContentControl^ container);
            Platform::Object^ GetItemFormIndex(int index);
        private:
            static void OnItemSourceChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e);
            static void OnItemTemplateChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e);
            static void OnItemTemplateSelectorChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e);
            static void OnItemContainerStyleChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e);
            static void OnItemContainerStyleSelectorChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e);

            void ApplyItemContainerStyle(WinCon::ContentControl^ container, Platform::Object^ item);
            void ApplyItemTemplate(WinCon::ContentControl^ container, Platform::Object^ item);

            EventRegistrationToken _collectionEventToken;
            void OnCollectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Interop::NotifyCollectionChangedEventArgs^ e);

            Vector<WinCon::ContentControl^>^ _recycledContainers;
            UnorderedMap<Platform::Object^, WinCon::ContentControl^,HashObject>^ _itemContainerMap;
            Vector<Platform::Object^>^ _items;
        };
    }
}

VirtualizingPanel 的具体实现

接下来是各方法的具体实现

VirtualizingPanel 构造函数

VirtualizingPanel::VirtualizingPanel()  
{
    RegisterDependencyProperties();
    _items = ref new Vector<Platform::Object^>();
    _recycledContainers = ref new Vector<WinCon::ContentControl^>();
    _itemContainerMap = ref new UnorderedMap<Platform::Object^, WinCon::ContentControl^, HashObject>();

    _items->VectorChanged += ref new Windows::Foundation::Collections::VectorChangedEventHandler<Platform::Object ^>(this, &MoePic::Controls::VirtualizingPanel::OnItemsChanged);
}

VirtualizingPanel 构造函数里面调用了注册依赖属性的方法,然后是字段的初始化,并且监听了 Items 列表的改变时间(话说 C++/CX 的 List 的替代物 Vector 直接就实现了内容改变的通知接口,真方便...)

RegisterDependencyProperties() 注册依赖属性

void VirtualizingPanel::RegisterDependencyProperties()  
{
    if (_itemContainerStyleProperty == nullptr)
    {
        _itemContainerStyleProperty = DependencyProperty::Register(
            nameof(ItemContainerStyle),
            typeof(Windows::UI::Xaml::Style),
            typeof(VirtualizingPanel),
            ref new PropertyMetadata(nullptr,
                ref new PropertyChangedCallback(
                    &VirtualizingPanel::OnItemContainerStyleChangedStatic)));
    }
    if (_itemContainerStyleSelectorProperty == nullptr)
    {
        _itemContainerStyleSelectorProperty = DependencyProperty::Register(
            nameof(ItemContainerStyleSelector),
            typeof(WinCon::StyleSelector),
            typeof(VirtualizingPanel),
            ref new PropertyMetadata(nullptr,
                ref new PropertyChangedCallback(
                    &VirtualizingPanel::OnItemContainerStyleChangedStatic)));
    }
    if (_itemSourceProperty == nullptr)
    {
        _itemSourceProperty = DependencyProperty::Register(
            nameof(ItemSource),
            typeof(Platform::Object),
            typeof(VirtualizingPanel),
            ref new PropertyMetadata(nullptr,
                ref new PropertyChangedCallback(
                    &VirtualizingPanel::OnItemSourceChangedStatic)));
    }
    if (_itemTemplateProperty == nullptr)
    {
        _itemTemplateProperty = DependencyProperty::Register(
            nameof(ItemTemplate),
            typeof(DataTemplate),
            typeof(VirtualizingPanel),
            ref new PropertyMetadata(nullptr,
                ref new PropertyChangedCallback(
                    &VirtualizingPanel::OnItemTemplateChangedStatic)));
    }
    if (_itemTemplateSelectorProperty == nullptr)
    {
        _itemTemplateSelectorProperty = DependencyProperty::Register(
            nameof(ItemTemplateSelector),
            typeof(WinCon::DataTemplateSelector),
            typeof(VirtualizingPanel),
            ref new PropertyMetadata(nullptr,
                ref new PropertyChangedCallback(
                    &VirtualizingPanel::OnItemTemplateSelectorChangedStatic)));
    }
}

没有什么需要特别说明的,就是很简单的注册依赖属性而已。

IsItemItsOwnContainerOverride(Platform::Object^) 判断子项是否已经是容器

bool VirtualizingPanel::IsItemItsOwnContainerOverride(Platform::Object^ obj)  
{
    auto container = dynamic_cast<WinCon::ContentControl^>(obj);
    return container != nullptr;
}

很简单的逻辑,直接尝试转换为 ContentControl^

GetContainerForItemOverride() 创建新的子项容器

WinCon::ContentControl^ VirtualizingPanel::GetContainerForItemOverride()  
{
    return ref new WinCon::ContentControl();
}

也没有什么需要说明的,直接 new 一个 ContentControl^ ,然后返回即可。

ClearContainerForItemOverride(ContentControl^, Object^) 清理容器,准备复用

void VirtualizingPanel::ClearContainerForItemOverride(WinCon::ContentControl^ container, Object^ item)  
{
    if (IsItemItsOwnContainerOverride(item))
    {
        return;
    }

    container->Content = nullptr;
    container->ContentTemplate = nullptr;
    container->Style = nullptr;
}

要注意的是,这里需要判断子项是否已经是容器,如果是,则不需要清理,我们仅需要清理由我们生成的项目。

PrepareContainerForItemOverride(ContentControl^, Object^) 为子项准备容器,准备呈现

void VirtualizingPanel::PrepareContainerForItemOverride(WinCon::ContentControl^ container, Object^ item)  
{
    if (IsItemItsOwnContainerOverride(item))
    {
        return;
    }

    container->Content = item;

    ApplyItemContainerStyle(container, item);
    ApplyItemTemplate(container, item);
}

和清理容器一样,我们仅需要变更由我们所控制的容器。

GetContainerFormItem(Object^) 由子项到容器的映射

WinCon::ContentControl^ VirtualizingPanel::GetContainerFormItem(Platform::Object^ item)  
{
    if (_itemContainerMap->HasKey(item))
    {
        return _itemContainerMap->Lookup(item);
    }
    else
    {
        return nullptr;
    }
}

由于我们有一个字典储存该映射,我们可以快速的在字典中查找。

GetContainerFormIndex(int) 由子项索引到容器的映射

WinCon::ContentControl^ VirtualizingPanel::GetContainerFormIndex(int index)  
{
    if (index < 0 || index >= _items->Size)
    {
        throw ref new Platform::OutOfBoundsException("Index out of bounds.");
    }

    auto item = _items->GetAt(index);
    return GetContainerFormItem(item);
}

首先由索引或得到子项,然后再获取到容器。

GetItemFormContainer(ContentControl^) 由容器到子项的映射

Platform::Object^ VirtualizingPanel::GetItemFormContainer(WinCon::ContentControl^ container)  
{
    if (container == nullptr)
    {
        return nullptr;
    }

    auto item = container->Content;

    if (_itemContainerMap->HasKey(item))
    {
        return item;
    }
    else
    {
        return nullptr;
    }
}

这就更加简单了,直接获取容器的 Content 属性即可。

GetItemFormIndex(int) 由子项索引到子项的映射

Platform::Object^ VirtualizingPanel::GetItemFormIndex(int index)  
{
    if (index < 0 || index >= _items->Size)
    {
        throw ref new Platform::OutOfBoundsException("Index out of bounds.");
    }

    return _items->GetAt(index);
}

没啥说的,_items 里面直接获取就可以了。

OnItemSourceChanged(Object^, Object^) 当 ItemSource 改变时的处理

void VirtualizingPanel::OnItemSourceChanged(Platform::Object^ newItems, Platform::Object^ oldItems)  
{
    this->RecycleAllItem();

    _items->Clear();

    auto items = dynamic_cast<Windows::UI::Xaml::Interop::IBindableIterable^>(newItems);

    if (items != nullptr)
    {
        auto itertor = items->First();

        while (itertor->HasCurrent)
        {
            _items->Append(itertor->Current);
            itertor->MoveNext();
        }
    }
    else
    {
        _items->Append(newItems);
    }

    auto nc = dynamic_cast<Windows::UI::Xaml::Interop::INotifyCollectionChanged^>(oldItems);
    if (nc != nullptr)
    {
        nc->CollectionChanged -= _collectionEventToken;
    }

    nc = dynamic_cast<Windows::UI::Xaml::Interop::INotifyCollectionChanged^>(newItems);
    if (nc != nullptr)
    {
        _collectionEventToken = nc->CollectionChanged += ref new Windows::UI::Xaml::Interop::NotifyCollectionChangedEventHandler(this, &MoePic::Controls::VirtualizingPanel::OnCollectionChanged);
    }
}

当 ItemSource 改变时,大致要做两件事,第一,清空并且加入新的列表项;第二,如果 ItemSource 实现了 INotifyCollectionChanged 接口,重新侦听 ItemSource 的改变事件。

OnCollectionChanged(Object^, NotifyCollectionChangedEventArgs^) 当 ItemSource 内容改变时的处理

void VirtualizingPanel::OnCollectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Interop::NotifyCollectionChangedEventArgs^ e)  
{
    auto itertor = e->NewItems->First();
    switch (e->Action)
    {
    case Windows::UI::Xaml::Interop::NotifyCollectionChangedAction::Add:
        while (itertor->HasCurrent)
        {
            _items->Append(itertor->Current);
            itertor->MoveNext();
        }
        break;
    case Windows::UI::Xaml::Interop::NotifyCollectionChangedAction::Move:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    case Windows::UI::Xaml::Interop::NotifyCollectionChangedAction::Remove:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    case Windows::UI::Xaml::Interop::NotifyCollectionChangedAction::Replace:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    case Windows::UI::Xaml::Interop::NotifyCollectionChangedAction::Reset:
        _items->Clear();
        break;
    default:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    }
}

由于为了使逻辑看起来简单,我们暂时就不做子项被删除,移动,替换的处理,仅包含添加,与清空。这部分只要将 ItemSource 与 Items 映射处理好就够了。

ApplyItemContainerStyle(ContentControl^, Object^) 为容器应用指定的样式

void VirtualizingPanel::ApplyItemContainerStyle(WinCon::ContentControl^ container, Platform::Object^ item)  
{
    if (ItemContainerStyleSelector != nullptr)
    {
        container->Style = ItemContainerStyleSelector->SelectStyle(item, container);
    }

    if (container->Style == nullptr)
    {
        container->Style = ItemContainerStyle;
    }
}

ItemContainerStyleSelector 是最优先处理的,其次才是 ItemContainerStyle。

ApplyItemTemplate(ContentControl^, Object^) 为容器应用指定的模板

void VirtualizingPanel::ApplyItemTemplate(WinCon::ContentControl^ container, Platform::Object^ item)  
{
    if (ItemTemplateSelector != nullptr)
    {
        container->ContentTemplate = ItemTemplateSelector->SelectTemplate(item);
    }

    if (container->ContentTemplate == nullptr)
    {
        container->ContentTemplate = ItemTemplate;
    }
}

同样的,ItemTemplateSelector 是优先于 ItemTemplate 的。

RecycleItem(Object^) 回收子项

void VirtualizingPanel::RecycleItem(Platform::Object^ item)  
{
    auto container = GetContainerFormItem(item);

    if (container == nullptr)
    {
        return;
    }

    unsigned int index = 0;
    if (Children->IndexOf(container, &index))
    {
        Children->RemoveAt(index);
        _itemContainerMap->Remove(item);
        ClearContainerForItemOverride(container, item);

        if (!IsItemItsOwnContainerOverride(item))
        {
            RecycledContainers->Append(container);
        }
    }
    else
    {
        throw Exception::CreateException(-1, "Can't found container in panel.");
    }
}

很简单,先尝试获取子项对应的容器,如果有,则表示已经被呈现,可以被回收,然后将其从可视化移出,映射字典中移出,清理容器,如果是我们生成的容器,则可以放入回收列表,以便复用。

ApplyItemTemplate(ContentControl^, Object^) 为容器应用指定的模板

WinCon::ContentControl^  VirtualizingPanel::RealizeItem(Platform::Object^ item)  
{
    WinCon::ContentControl^ container = nullptr;

    if (_itemContainerMap->HasKey(item))
    {
        return _itemContainerMap->Lookup(item);
    }

    if (!IsItemItsOwnContainerOverride(item))
    {
        if (RecycledContainers->Size > 0)
        {
            container = RecycledContainers->GetAt(RecycledContainers->Size - 1);
            RecycledContainers->RemoveAtEnd();
        }
        else
        {
            container = GetContainerForItemOverride();
        }
    }
    else
    {
        container = dynamic_cast<WinCon::ContentControl^>(item);
    }

    PrepareContainerForItemOverride(container, item);
    _itemContainerMap->Insert(item, container);
    Children->Append(container);

    return container;
}

先尝试获取子项对应的容器,如果有,则表示已经被呈现,不需要再实例化,直接返回容器即可。如果子项就包含容器,就不需要生成容器,否则就为子项生成一个容器,接下来就是加入可视化树,并加入到子项容器字典中。

RecycleAllItem() 回收所有子项

void VirtualizingPanel::RecycleAllItem()  
{
    for each (auto items in _itemContainerMap)
    {
        RecycleItem(items->Key);
    }
}

遍历,子项容器字典,然后回收即可。

完整的 VirtualizingPanel.cpp 文件

一些重要的方法说明完毕,下面是完整的 VirtualizingPanel.cpp 文件

#include "pch.h"
#include "VirtualizingPanel.h"

using namespace MoePic::Controls;

DependencyProperty^ VirtualizingPanel::_itemContainerStyleProperty = nullptr;  
DependencyProperty^ VirtualizingPanel::_itemContainerStyleSelectorProperty = nullptr;  
DependencyProperty^ VirtualizingPanel::_itemSourceProperty = nullptr;  
DependencyProperty^ VirtualizingPanel::_itemTemplateProperty = nullptr;  
DependencyProperty^ VirtualizingPanel::_itemTemplateSelectorProperty = nullptr;

VirtualizingPanel::VirtualizingPanel()  
{
    RegisterDependencyProperties();
    _items = ref new Vector<Platform::Object^>();
    _recycledContainers = ref new Vector<WinCon::ContentControl^>();
    _itemContainerMap = ref new UnorderedMap<Platform::Object^, WinCon::ContentControl^, HashObject>();

    _items->VectorChanged += ref new Windows::Foundation::Collections::VectorChangedEventHandler<Platform::Object ^>(this, &MoePic::Controls::VirtualizingPanel::OnItemsChanged);
}

void VirtualizingPanel::RegisterDependencyProperties()  
{
    if (_itemContainerStyleProperty == nullptr)
    {
        _itemContainerStyleProperty = DependencyProperty::Register(
            nameof(ItemContainerStyle),
            typeof(Windows::UI::Xaml::Style),
            typeof(VirtualizingPanel),
            ref new PropertyMetadata(nullptr,
                ref new PropertyChangedCallback(
                    &VirtualizingPanel::OnItemContainerStyleChangedStatic)));
    }
    if (_itemContainerStyleSelectorProperty == nullptr)
    {
        _itemContainerStyleSelectorProperty = DependencyProperty::Register(
            nameof(ItemContainerStyleSelector),
            typeof(WinCon::StyleSelector),
            typeof(VirtualizingPanel),
            ref new PropertyMetadata(nullptr,
                ref new PropertyChangedCallback(
                    &VirtualizingPanel::OnItemContainerStyleChangedStatic)));
    }
    if (_itemSourceProperty == nullptr)
    {
        _itemSourceProperty = DependencyProperty::Register(
            nameof(ItemSource),
            typeof(Platform::Object),
            typeof(VirtualizingPanel),
            ref new PropertyMetadata(nullptr,
                ref new PropertyChangedCallback(
                    &VirtualizingPanel::OnItemSourceChangedStatic)));
    }
    if (_itemTemplateProperty == nullptr)
    {
        _itemTemplateProperty = DependencyProperty::Register(
            nameof(ItemTemplate),
            typeof(DataTemplate),
            typeof(VirtualizingPanel),
            ref new PropertyMetadata(nullptr,
                ref new PropertyChangedCallback(
                    &VirtualizingPanel::OnItemTemplateChangedStatic)));
    }
    if (_itemTemplateSelectorProperty == nullptr)
    {
        _itemTemplateSelectorProperty = DependencyProperty::Register(
            nameof(ItemTemplateSelector),
            typeof(WinCon::DataTemplateSelector),
            typeof(VirtualizingPanel),
            ref new PropertyMetadata(nullptr,
                ref new PropertyChangedCallback(
                    &VirtualizingPanel::OnItemTemplateSelectorChangedStatic)));
    }
}

bool VirtualizingPanel::IsItemItsOwnContainerOverride(Platform::Object^ obj)  
{
    auto container = dynamic_cast<WinCon::ContentControl^>(obj);
    return container != nullptr;
}

WinCon::ContentControl^ VirtualizingPanel::GetContainerForItemOverride()  
{
    return ref new WinCon::ContentControl();
}

void VirtualizingPanel::ClearContainerForItemOverride(WinCon::ContentControl^ container, Object^ item)  
{
    if (IsItemItsOwnContainerOverride(item))
    {
        return;
    }

    container->Content = nullptr;
    container->ContentTemplate = nullptr;
    container->Style = nullptr;
}

void VirtualizingPanel::PrepareContainerForItemOverride(WinCon::ContentControl^ container, Object^ item)  
{
    if (IsItemItsOwnContainerOverride(item))
    {
        return;
    }

    container->Content = item;

    ApplyItemContainerStyle(container, item);

    ApplyItemTemplate(container, item);
}

WinCon::ContentControl^ VirtualizingPanel::GetContainerFormItem(Platform::Object^ item)  
{
    if (_itemContainerMap->HasKey(item))
    {
        return _itemContainerMap->Lookup(item);
    }
    else
    {
        return nullptr;
    }
}

WinCon::ContentControl^ VirtualizingPanel::GetContainerFormIndex(int index)  
{
    if (index < 0 || index >= _items->Size)
    {
        throw ref new Platform::OutOfBoundsException("Index out of bounds.");
    }

    auto item = _items->GetAt(index);
    return GetContainerFormItem(item);
}

Platform::Object^ VirtualizingPanel::GetItemFormContainer(WinCon::ContentControl^ container)  
{
    if (container == nullptr)
    {
        return nullptr;
    }

    auto item = container->Content;

    if (_itemContainerMap->HasKey(item))
    {
        return item;
    }
    else
    {
        return nullptr;
    }
}

Platform::Object^ VirtualizingPanel::GetItemFormIndex(int index)  
{
    if (index < 0 || index >= _items->Size)
    {
        throw ref new Platform::OutOfBoundsException("Index out of bounds.");
    }

    return _items->GetAt(index);
}

void VirtualizingPanel::OnItemSourceChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e)  
{
    auto panel = dynamic_cast<VirtualizingPanel^>(sender);

    if (panel == nullptr)
    {
        return;
    }

    panel->OnItemSourceChanged(e->NewValue, e->OldValue);
}

void VirtualizingPanel::OnItemTemplateChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e)  
{
    auto panel = dynamic_cast<VirtualizingPanel^>(sender);

    if (panel == nullptr)
    {
        return;
    }

    panel->OnItemTemplateChanged(dynamic_cast<DataTemplate^>(e->NewValue), dynamic_cast<DataTemplate^>(e->OldValue));
}

void VirtualizingPanel::OnItemTemplateSelectorChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e)  
{
    auto panel = dynamic_cast<VirtualizingPanel^>(sender);

    if (panel == nullptr)
    {
        return;
    }

    panel->OnItemTemplateSelectorChanged(dynamic_cast<WinCon::DataTemplateSelector^>(e->NewValue), dynamic_cast<WinCon::DataTemplateSelector^>(e->OldValue));
}

void VirtualizingPanel::OnItemContainerStyleChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e)  
{
    auto panel = dynamic_cast<VirtualizingPanel^>(sender);

    if (panel == nullptr)
    {
        return;
    }

    panel->OnItemContainerStyleChanged(dynamic_cast<Windows::UI::Xaml::Style^>(e->NewValue), dynamic_cast<Windows::UI::Xaml::Style^>(e->OldValue));
}

void VirtualizingPanel::OnItemContainerStyleSelectorChangedStatic(DependencyObject^ sender, Windows::UI::Xaml::DependencyPropertyChangedEventArgs^ e)  
{
    auto panel = dynamic_cast<VirtualizingPanel^>(sender);

    if (panel == nullptr)
    {
        return;
    }

    panel->OnItemContainerStyleSelectorChanged(dynamic_cast<WinCon::StyleSelector^>(e->NewValue), dynamic_cast<WinCon::StyleSelector^>(e->OldValue));
}

void VirtualizingPanel::OnItemContainerStyleChanged(Windows::UI::Xaml::Style^ newStyle, Windows::UI::Xaml::Style^ oldStyle)  
{
    for each (auto var in _itemContainerMap)
    {
        ApplyItemContainerStyle(var->Value, var->Key);
    }
}

void VirtualizingPanel::OnItemContainerStyleSelectorChanged(WinCon::StyleSelector^ newStyleSelector, WinCon::StyleSelector^ oldStyleSelector)  
{
    for each (auto var in _itemContainerMap)
    {
        ApplyItemContainerStyle(var->Value, var->Key);
    }
}

void VirtualizingPanel::OnItemTemplateChanged(DataTemplate^ newTemplate, DataTemplate^ oldTemplate)  
{
    for each (auto var in _itemContainerMap)
    {
        ApplyItemTemplate(var->Value, var->Key);
    }
}

void VirtualizingPanel::OnItemTemplateSelectorChanged(WinCon::DataTemplateSelector^ newTemplateSelector, WinCon::DataTemplateSelector^ oldTemplateSelector)  
{
    for each (auto var in _itemContainerMap)
    {
        ApplyItemTemplate(var->Value, var->Key);
    }
}

void VirtualizingPanel::OnItemSourceChanged(Platform::Object^ newItems, Platform::Object^ oldItems)  
{
    this->RecycleAllItem();

    _items->Clear();

    auto items = dynamic_cast<Windows::UI::Xaml::Interop::IBindableIterable^>(newItems);

    if (items != nullptr)
    {
        auto itertor = items->First();

        while (itertor->HasCurrent)
        {
            _items->Append(itertor->Current);
            itertor->MoveNext();
        }
    }
    else
    {
        _items->Append(newItems);
    }

    auto nc = dynamic_cast<Windows::UI::Xaml::Interop::INotifyCollectionChanged^>(oldItems);
    if (nc != nullptr)
    {
        nc->CollectionChanged -= _collectionEventToken;
    }

    nc = dynamic_cast<Windows::UI::Xaml::Interop::INotifyCollectionChanged^>(newItems);
    if (nc != nullptr)
    {
        _collectionEventToken = nc->CollectionChanged += ref new Windows::UI::Xaml::Interop::NotifyCollectionChangedEventHandler(this, &MoePic::Controls::VirtualizingPanel::OnCollectionChanged);
    }
}

void VirtualizingPanel::OnCollectionChanged(Platform::Object^ sender, Windows::UI::Xaml::Interop::NotifyCollectionChangedEventArgs^ e)  
{
    auto itertor = e->NewItems->First();
    switch (e->Action)
    {
    case Windows::UI::Xaml::Interop::NotifyCollectionChangedAction::Add:
        while (itertor->HasCurrent)
        {
            _items->Append(itertor->Current);
            itertor->MoveNext();
        }
        break;
    case Windows::UI::Xaml::Interop::NotifyCollectionChangedAction::Move:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    case Windows::UI::Xaml::Interop::NotifyCollectionChangedAction::Remove:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    case Windows::UI::Xaml::Interop::NotifyCollectionChangedAction::Replace:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    case Windows::UI::Xaml::Interop::NotifyCollectionChangedAction::Reset:
        _items->Clear();
        break;
    default:
        throw Exception::CreateException(-1, "Unexpected collection operation.");
        break;
    }
}

void VirtualizingPanel::OnItemsChanged(IObservableVector<Platform::Object^>^ source, IVectorChangedEventArgs^ e)  
{
    return;
}

void VirtualizingPanel::ApplyItemContainerStyle(WinCon::ContentControl^ container, Platform::Object^ item)  
{
    if (ItemContainerStyleSelector != nullptr)
    {
        container->Style = ItemContainerStyleSelector->SelectStyle(item, container);
    }

    if (container->Style == nullptr)
    {
        container->Style = ItemContainerStyle;
    }
}

void VirtualizingPanel::ApplyItemTemplate(WinCon::ContentControl^ container, Platform::Object^ item)  
{
    if (ItemTemplateSelector != nullptr)
    {
        container->ContentTemplate = ItemTemplateSelector->SelectTemplate(item);
    }

    if (container->ContentTemplate == nullptr)
    {
        container->ContentTemplate = ItemTemplate;
    }
}

void VirtualizingPanel::RecycleItem(Platform::Object^ item)  
{
    auto container = GetContainerFormItem(item);

    if (container == nullptr)
    {
        return;
    }

    unsigned int index = 0;
    if (Children->IndexOf(container, &index))
    {
        Children->RemoveAt(index);
        _itemContainerMap->Remove(item);
        ClearContainerForItemOverride(container, item);

        if (!IsItemItsOwnContainerOverride(item))
        {
            RecycledContainers->Append(container);
        }
    }
    else
    {
        throw Exception::CreateException(-1, "Can't found container in panel.");
    }
}

WinCon::ContentControl^  VirtualizingPanel::RealizeItem(Platform::Object^ item)  
{
    WinCon::ContentControl^ container = nullptr;

    if (_itemContainerMap->HasKey(item))
    {
        return _itemContainerMap->Lookup(item);
    }

    if (!IsItemItsOwnContainerOverride(item))
    {
        if (RecycledContainers->Size > 0)
        {
            container = RecycledContainers->GetAt(RecycledContainers->Size - 1);
            RecycledContainers->RemoveAtEnd();
        }
        else
        {
            container = GetContainerForItemOverride();
        }
    }
    else
    {
        container = dynamic_cast<WinCon::ContentControl^>(item);
    }

    PrepareContainerForItemOverride(container, item);
    _itemContainerMap->Insert(item, container);
    Children->Append(container);

    return container;
}

void VirtualizingPanel::RecycleAllItem()  
{
    for each (auto items in _itemContainerMap)
    {
        RecycleItem(items->Key);
    }
}

还有一些关于依赖属性的变更通知我就没有详细说明了,这些东西看一下基本就很清楚了。

虚拟化的详细过程

那么我们的 VirtualizingPanel 已经构建好了,那么它是如何进行工作的呢?

01.首先是,我们需要获取要被实例化的子项。

02.然后调用 RealizeItem(Object^) 方法来实例化它们。

03.RealizeItem(Object^) 方法会为子项准备容器,优先使用回收掉的容器,然后添加到可视化中。

04.完成测量与布局操作。

05.当视图更新的时候,也就是 ScrollViewer 在滚动的时候,亦或是项目更改的时候,重新计算要被实例化的子项。

06.调用 RecycleItem(Object^) 方法回收不需要的子项。

07.RecycleItem(Object^) 方法能清理容器并加入回收列表中。

08.回到第二步,实例化需要实例化的子项。

虚拟化的逻辑大致如此,第一部分也到此结束,接下来的第二个部分将会描述如何使用 VirtualizingPanel 作为基类,来创建一个瀑布流布局支持虚拟化的面板。在这个部分中将会着重讲述如何将虚拟化的逻辑与测量与布局操作结合在一起,并且在布局中,我们可以做哪些优化。敬请期待!

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章