Skip to content

WinUI 3 C++/WinRT 高级数据绑定与 MVVM 架构实践(第 2 篇:高级绑定与 ViewModel 结构深化)

衔接自第1篇:已具备 BaseViewModel / RelayCommand / 基础集合与属性通知。本文聚焦:项目目录规划、IDL 拆分策略、进阶绑定(值转换/多源组合/复杂控件)、附加属性与 DependencyProperty 融合 MVVM、典型实体建模与分层放置决策。

1. 目录与分层策略(完整落地)

推荐在解决方案根(示例:src/)采用如下结构:

src/
  App.xaml / App.idl / App.xaml.*
  Models/
    Domain/               // 纯领域实体 (无 WinRT 依赖,可单元测试)
      User.h
      Order.h
      Product.h
    Dtos/                 // 传输结构(与后端 API / 序列化绑定)
      UserDto.h
      PagedResult.h
    Contracts.idl         // 需要暴露到 XAML 绑定的 WinRT 可见模型(少量)
  Services/
    Abstractions/         // 接口 (C++ 纯虚 or WinRT interface)
      IUserRepository.h
      INetworkClient.h
    Implementations/
      UserRepository.cpp
      NetworkClient.cpp
  ViewModels/
    Base/
      BaseViewModel.idl
      BaseViewModel.h/.cpp
      RelayCommand.h
    Home/
      HomeViewModel.idl
      HomeViewModel.h/.cpp
    Users/
      UsersListViewModel.idl
      UsersListViewModel.h/.cpp
      UserDetailViewModel.idl
      UserDetailViewModel.h/.cpp
  Views/
    HomePage.xaml/.h/.cpp/.idl
    UsersPage.xaml/.h/.cpp/.idl
    UserDetailPage.xaml/.h/.cpp/.idl
  Controls/
    ValidatingTextBox.xaml/.idl/.h/.cpp    // 自定义控件
  Resources/
    Strings/zh-CN/Resources.resw
    Themes/Generic.xaml
  Infrastructure/
    ServiceLocator.h/.cpp
    MessageBus.h/.h
  Diagnostics/
    Logging.h

1.1 Model / ViewModel / View 放什么?

放置内容绝不放说明
Model.Domain领域状态 + 业务不变式XAML/WinRT 依赖纯 C++,利于测试/复用
Model.Dtos序列化映射结构逻辑与 JSON/XML 映射
ViewModel可绑定属性 + 命令 + 协调服务调用复杂渲染/控件实例只输出 primitive / hstring / enum / IObservableVector / bool / DateTime 等
View(XAML)UI 布局 + 轻度交互 (导航)业务规则事件转 Command 或 minimal code-behind
ServiceIO / 网络 / 缓存 / 仓储UI 控件提供异步 API 给 VM
InfrastructureDI / 消息总线 / 主题切换领域规则基础设施支持

1.2 哪些需要写 IDL?

场景是否写 IDL理由
被 XAML {x:Bind} 直接访问的 runtimeclass需生成 .winmd 供编译期绑定解析
仅内部 C++ 使用的纯类 (领域实体)降低开销,保持纯净
需要被多语言组件消费WinRT 边界需要元数据
自定义控件公开 DependencyProperty让 XAML 可识别类型
命令实现 RelayCommand (内部)通过 winrt::make 工厂创建,无需 IDL

1.3 IDL 继承策略

  • ViewModel 基类:继承 Windows.UI.Xaml.Data.INotifyPropertyChanged
  • 需要支持集合:仅属性返回 IObservableVector<T>,无需额外继承
  • 附加属性提供者:注册为静态 runtimeclass(若只写静态附加属性,可不实例化)
  • 自定义控件:继承 Microsoft.UI.Xaml.Controls.Control 或派生控件基类

示例:UsersListViewModel.idl

idl
import "BaseViewModel.idl";
namespace MyApp.ViewModels
{
    runtimeclass UsersListViewModel : BaseViewModel
    {
        UsersListViewModel();
        Windows.Foundation.Collections.IObservableVector<String> Users{ get; };
        String SelectedUserId;
        Boolean IsLoading;
        String ErrorMessage;
        // 命令方法
        void RefreshCommand();
        void LoadMoreCommand();
    };
}

2. 高级绑定策略

2.1 计算/派生属性缓存

在频繁刷新 UI 的场景避免重复拼接:

cpp
// UsersListViewModel.h (片段)
struct UsersListViewModel : UsersListViewModelT<UsersListViewModel, BaseViewModel>
{
    // ...existing code...
private:
    hstring m_selectedUserId;
    bool m_isLoading{false};
    hstring m_error;
    // 派生缓存
    hstring m_statusBarCache;

    void RecomputeStatusBar()
    {
        // O(1) 构造,避免 List 遍历
        m_statusBarCache = L"共 " + winrt::to_hstring(m_users.Size()) + L" 人" +
            (m_selectedUserId.empty()? L"" : L",选中=" + m_selectedUserId);
        RaisePropertyChanged(L"StatusBarText");
    }
public:
    hstring StatusBarText() const { return m_statusBarCache; }

    void SelectedUserId(hstring const& v)
    {
        if (SetProperty(m_selectedUserId, v, L"SelectedUserId"))
            RecomputeStatusBar();
    }
};

2.2 多源组合(无官方 MultiBinding)

通过函数:

xml
<TextBlock Text="{x:Bind ViewModel.FormatUserLine(ViewModel.SelectedUserId, ViewModel.IsLoading), Mode=OneWay}"/>
cpp
hstring UsersListViewModel::FormatUserLine(hstring const& id, bool loading)
{
    if (loading) return L"加载中...";
    if (id.empty()) return L"未选中";
    return L"当前用户: " + id;
}

注意:任一参数属性变化会触发函数重新求值(编译期生成表达式订阅);保持函数纯净。参见 data-binding-basics.md §10。

2.3 自定义值转换替代方案

优先 x:Bind 函数;若需在资源字典重用或必须使用 Binding:

cpp
struct BoolToVisibilityConverter : winrt::implements<BoolToVisibilityConverter, winrt::IValueConverter>
{
    winrt::IInspectable Convert(winrt::IInspectable const& v, winrt::TypeName const&, winrt::IInspectable const&, hstring const&)
    {
        bool b = winrt::unbox_value<bool>(v);
        return winrt::box_value(b? winrt::Visibility::Visible : winrt::Visibility::Collapsed);
    }
    winrt::IInspectable ConvertBack(winrt::IInspectable const&, winrt::TypeName const&, winrt::IInspectable const&, hstring const&) { return nullptr; }
};

XAML 引用:

xml
<Page.Resources>
    <local:BoolToVisibilityConverter x:Key="BoolToVis"/>
</Page.Resources>
<TextBlock Text="正在加载" Visibility="{Binding IsLoading, Converter={StaticResource BoolToVis}}"/>

2.4 复杂控件(ListView + 增量加载)

ViewModel 暴露实现 ISupportIncrementalLoading 的集合封装(详见 第六部分教程 + 第1篇中引用示例):

cpp
struct IncrementalUsersSource : winrt::implements<IncrementalUsersSource,
    winrt::IObservableVector<hstring>, winrt::ISupportIncrementalLoading>
{
    // ...省略基础集合实现(参考 winui3-advanced-binding 第1篇 和 collection-binding.md)
    bool HasMoreItems() const { return m_hasMore; }
    winrt::IAsyncOperation<winrt::LoadMoreItemsResult> LoadMoreItemsAsync(uint32_t count)
    {
        co_await winrt::resume_background();
        // 拉取数据 -> append
        co_return winrt::LoadMoreItemsResult{ loaded }; // UI 自动刷新
    }
};

ViewModel:

cpp
auto Users() const { return m_incrementalUsers; } // 直接绑定 ItemsSource

ListView:

xml
<ListView ItemsSource="{x:Bind ViewModel.Users}" IncrementalLoadingTrigger="Edge" IncrementalLoadingThreshold="5"/>

2.5 附加属性 + MVVM 行为注入

自定义“自动滚动到底”行为: AutoScrollBehavior.idl

idl
namespace MyApp.Attached
{
    [default_interface]
    runtimeclass AutoScrollBehavior : Microsoft.UI.Xaml.DependencyObject
    {
        AutoScrollBehavior();
        static Microsoft.UI.Xaml.DependencyProperty EnableProperty{ get; };
        static Boolean GetEnable(Microsoft.UI.Xaml.DependencyObject target);
        static void SetEnable(Microsoft.UI.Xaml.DependencyObject target, Boolean value);
    };
}

实现:

cpp
// AutoScrollBehavior.cpp (片段)
DependencyProperty AutoScrollBehavior::s_enableProperty =
    DependencyProperty::RegisterAttached(L"Enable", xaml_typename<bool>(), xaml_typename<MyApp::Attached::AutoScrollBehavior>(),
        PropertyMetadata{ winrt::box_value(false), PropertyChangedCallback{ &AutoScrollBehavior::OnEnableChanged }});

void AutoScrollBehavior::OnEnableChanged(DependencyObject const& d, DependencyPropertyChangedEventArgs const& e)
{
    if (auto list = d.try_as<winrt::ListView>())
    {
        bool enabled = winrt::unbox_value<bool>(e.NewValue());
        if (enabled)
        {
            // 订阅集合变化
            if (auto ov = list.ItemsSource().try_as<winrt::IObservableVector<IInspectable>>())
            {
                auto weak = winrt::make_weak(list);
                ov.VectorChanged([weak](auto const& src, auto const& args)
                {
                    if (auto strong = weak.get())
                    {
                        strong.UpdateLayout();
                        strong.ScrollIntoView(src.GetAt(src.Size()-1));
                    }
                });
            }
        }
    }
}

XAML 使用:

xml
<ListView ItemsSource="{x:Bind ViewModel.Logs}" local:AutoScrollBehavior.Enable="True"/>

附加属性放在 Attached 子命名空间;只需在 IDL 声明公开静态成员即可。详见 dependency-attached-properties.md 原理章节。

3. 异步协程与命令耦合(预告第3篇)

在第3篇将扩展:

cpp
winrt::IAsyncAction UsersListViewModel::RefreshCommand()
{
    if (m_isLoading) co_return; // 防抖
    IsLoading(true);
    auto guard = wil::scope_exit([&]{ IsLoading(false); });
    try
    {
        auto data = co_await m_userRepo->FetchUsersAsync();
        m_users.Clear();
        for (auto&& u : data) m_users.Append(u.Id);
        RaisePropertyChanged(L"Users");
    }
    catch (winrt::hresult_error const& ex)
    {
        ErrorMessage(ex.message());
    }
}

并给出统一错误转换 / 取消 Token / 超时包装模式。

4. 放置决策速表(补充)

需求放置示例
登录状态(当前用户信息)SessionService / ViewModel 暴露投影CurrentUserViewModel
UI 层缓存(列表分页)ViewModel 内部字段m_pageIndex, m_buffer
领域计算(价格折扣)Domain Model 方法Order::ApplyDiscount(rate)
UI 状态(IsBusy, DialogOpen)ViewModel 属性bool + RaisePropertyChanged
主题切换 / 全局消息Infrastructure (MessageBus/ThemeService)ThemeService::SetDark()
附加行为(AutoScroll)Attached runtimeclassAutoScrollBehavior

5. 已覆盖与外部参阅映射

本篇主题已引用基础外部文件
IDL 拆分策略接口机制interface.md
IObservableVector 使用集合差量collection-binding.md
SetProperty 模板INPC 原理property-change-notification.md
附加属性实现DependencyProperty 流程dependency-attached-properties.md
函数组合绑定x:Bind 工作模型data-binding-basics.md
增量加载骨架性能部分WinUI3-WinRT-CPP-完整教程-第六部分-实战技巧与最佳实践.md

6. 下一篇预告(第3篇)

将覆盖:

  • 协程与命令融合(AsyncRelayCommand in C++/WinRT)
  • 错误/取消/超时/重试策略矩阵
  • 消息与事件聚合(弱引用 + 频道分发)
  • 服务注入:手写 ServiceLocator vs. 基于 guid map 的容器
  • 复合 ViewModel(主从结构 / 工作单元模式)
  • 测试:Mock Repository & 协程调度注入

(第2篇完)