Avalonia 中自定义 ViewLocator 实现导航切换

005 Avalonia 中自定义 ViewLocator 实现导航切换

1. 前言

在 .NET 中实现页面的切换不像 Web 前端那样的方便,如何优雅的实现导航切换又是一件很是头疼的事情,我在我自己的WPF里面,光是切换页面这件事就有各种各样的写法。
本文将收集一些 Avalonia 中可行的页面切换方案,特别是实现侧栏导航效果。

我知道侧栏导航使用 TabControl 这种方式算是不错的选择,但是如果涉及普通跳转导航切换,TabControl 无法承载自定义的其他视图,所以使用 TabControl 是一件不太可行的事情。

2. 准备

我们打算做以下的几页呈现,主要的内容包括:首页、收藏页、用户信息页、设置页和内容页,主要的原型是一个音乐播放器。

3. 如何进行简单的导航?

导航的基础我们参考自这里 https://docs.avaloniaui.net/zh-Hans/docs/tutorials/todo-list-app/navigate-views

但是也仅是参考,实际上我们可能需要做一个 ViewLocator 视图定位器来进行页面的导航。

视图定位器不是什么很新的概念,我记得在很多 MVVM 框架里面就有类似的东西,虽然我从来没有怎么研究过,我会觉得被 ViewLocator 限制过于束手束脚了,不是很灵活,还是直接在 DataContext 里面 new 出 ViewModel 会亲和一些,但是 ViewLocator 确实是一种比较科学的管理方式。

ViewLocator 本质上干的是将 ViewModel 去匹配对应的 View 这件事,很多时候,大家会通过建立字典或者直接反射来实现,这两种都是不错的方式。在 Avalonia 的示例里面用到的机制是 ContentControl 的 DataTemplate 填入内容可以显示视图的特别机制,这在 WPF 里面也足够常见,但是似乎确实没有什么文章看到。

4. 我的 ViewLocator

我在项目中直接加入了一个 ViewLocator.cs 文件。

此外加入了若干的 View 和 ViewModel,这些都是最简单的 View 和 ViewModel,我们来看一对示例就知道了。

ViewModels/HomeViewModel.cs

namespace Test01.ViewModels
{
    public class HomeViewModel:ViewModelBase
    {
    }
}

Views/HomeView.axaml,我将其背景设置成了红色,注意看 Background = "Red"

<UserControl x:Class="Test01.Views.HomeView"
             xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             d:DesignHeight="450"
             d:DesignWidth="800"
             Background="Red"
             mc:Ignorable="d">
    Welcome to Avalonia!
</UserControl>

总之我们有这些:你可以按你自己的喜欢来创建。

下面就是 ViewLocator 的具体内容,代码如下:


using Avalonia.Controls;
using Avalonia.Controls.Templates;
using System;
using Test01.ViewModels;
using Test01.Views;

namespace Test01
{
    ///<summary>视图定位器。</summary>
    public class ViewLocator : IDataTemplate
    {
        public Control? Build(object? param)
        {
            if (param is MainViewModel) return new MainView();
            if (param is CollectionViewModel) return new CollectionView();
            if (param is HomeViewModel) return new HomeView();
            if (param is OptionSettingConfigViewModel) return new OptionSettingConfigView();
            if (param is UserInfoViewModel) return new UserInfoView();
            if (param is VideoContentViewModel) return new VideoContentView();
            throw new NotImplementedException();
        }

        public bool Match(object? data)
        {
            return data is ViewModelBase;
        }
    }
}


我们来对比一下 Avalonia 文档的内容:

using Avalonia.Controls;
using Avalonia.Controls.Templates;
using System;
using ToDoList.ViewModels;

namespace ToDoList
{
    public class ViewLocator : IDataTemplate
    {
        public Control Build(object data)
        {
            var name = data.GetType().FullName!.Replace("ViewModel", "View");
            var type = Type.GetType(name);

            if (type != null)
            {
                return (Control)Activator.CreateInstance(type)!;
            }
            else
            {
                return new TextBlock { Text = "Not Found: " + name };
            }
        }

        public bool Match(object data)
        {
            return data is ViewModelBase;
        }
    }
}

本质上就是一个指定 ViewModel 转为对应的 View 的转换器,为特定的 ViewModel 实例出自己对应的 View,剩下无论是写死,还是结合特性,还是用类名整理来反射的方式进行组织,都是可以的方案,实际实现也无非是这些而已。

随后将其放在 App.xaml 中

<Application x:Class="Test01.App"
             xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="using:Test01"
             RequestedThemeVariant="Default">
    <!--  "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options.  -->

    <Application.DataTemplates>
        <local:ViewLocator />
    </Application.DataTemplates>

    <Application.Styles>
        <FluentTheme />
    </Application.Styles>
</Application>

5. Shell 化

我们将 MainViewModel 作为全局的上下文,对应的 MainView 是整个应用程序最为顶层的界面。

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace Test01.ViewModels;

public partial class MainViewModel : ViewModelBase
{
    #region props

    [ObservableProperty]
    private ViewModelBase _currentViewModel;

    [ObservableProperty]
    private HomeViewModel _homeViewModel = new HomeViewModel();

    [ObservableProperty]
    private CollectionViewModel _collectionViewModel = new CollectionViewModel();

    [ObservableProperty]
    private UserInfoViewModel _userInfoViewModel = new UserInfoViewModel();

    [ObservableProperty]
    private OptionSettingConfigViewModel _optionSettingConfigViewModel = new OptionSettingConfigViewModel();

    [ObservableProperty]
    private VideoContentViewModel _videoContentViewModel = new VideoContentViewModel();

    #endregion

    #region ctors

    public MainViewModel()
    {
        CurrentViewModel = HomeViewModel;
    }

    #endregion

    #region methods

    [RelayCommand]
    public void GotoHome()
    {
        CurrentViewModel = HomeViewModel;
    }
    
    [RelayCommand]
    public void GotoCollection()
    {
        CurrentViewModel = CollectionViewModel;
    }

    [RelayCommand]
    public void GotoUserInfo()
    {
        CurrentViewModel = UserInfoViewModel;
    }

    #endregion

}

MainView.xaml 的定义如下:

<UserControl x:Class="Test01.Views.MainView"
             xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:app="using:Test01"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="clr-namespace:Test01.ViewModels"
             d:DesignHeight="450"
             d:DesignWidth="800"
             x:DataType="vm:MainViewModel"
             mc:Ignorable="d">

    <UserControl.DataContext>
        <vm:MainViewModel />
    </UserControl.DataContext>

    <Grid ColumnDefinitions="1*,1*">
        <StackPanel>
            <Button Width="80"
                    Height="30"
                    Command="{Binding GotoHomeCommand}" />
            <Button Width="80"
                    Height="30"
                    Command="{Binding GotoCollectionCommand}" />
            <Button Width="80"
                    Height="30"
                    Command="{Binding GotoUserInfoCommand}" />
        </StackPanel>
        <ContentControl Grid.Column="1" Content="{Binding CurrentViewModel}" />
    </Grid>


</UserControl>

MainWindow.xaml 的定义如下,就是默认的样子,其他都没有什么改动:

<Window x:Class="Test01.Views.MainWindow"
        xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:views="clr-namespace:Test01.Views"
        xmlns:vm="using:Test01.ViewModels"
        Title="Test01"
        Width="300"
        Height="200"
        Icon="/Assets/avalonia-logo.ico"
        mc:Ignorable="d">

    <views:MainView />


</Window>

6. 简单搭起的样子

posted @ 2024-05-14 17:48  fanbal  阅读(3881)  评论(4)    收藏  举报