文章

SukiUI 踩坑记录 0x01

SukiUI 踩坑记录 0x01

很久之前,我刚开始学习 Avalonia 的时候,决定开发一个工具包来满足日常需求,当时采用的 UI框架是 Material.Avalonia。但是我对这个框架并不满意,比如说他没有合适的导航菜单,我一直使用的ListBox 模拟,比如他的 ComboBox 有明显bug但是一两年过去都没有修。恰好前些天看到SukiUI基本解决了性能问题,于是决定尝试一下

替换导航菜单

替换导航菜单本来应该是一件简单的事情,可是他的文档实在太少了

SukiUI 提供了一个 SideMenu 作为导航菜单使用,这个菜单貌似很简单,一个 SideMenu 里有若干个 SideMenuItem,另外提供菜单头菜单脚的设置,还附带一个搜索框。

先看一下我的 ViewModel 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MainViewModel : ViewModelBase
{
    public List<MenuItemViewModel> Menus { get; } = [];
    
    // ...
}

public class MenuItemViewModel : ViewModelBase
{
    public string Name { get; set => this.RaiseAndSetIfChanged(ref field, value); } = "";

    public Material.Icons.MaterialIconKind IconKind { get; set => this.RaiseAndSetIfChanged(ref field, value); }

    [field:MaybeNull]
    public ViewModelBase ViewModel { get => field ??= VmFactory(); }
    
    public required Func<ViewModelBase> VmFactory { get; init; }
}

很简单的结构,Menus 里存放所有菜单信息,由于是小工具集合,所以希望每个菜单都是懒加载的,提升一点点启动速度。

0x01 先尝试把菜单弄出来

1
2
<suki:SukiSideMenu ItemsSource="{Binding Menus}">
</suki:SukiSideMenu>

运行!

成功报错

好家伙,直接异常了可还行,多少有点离谱了。

是我没写模板导致吗?先写一个TextBlock看看

1
2
3
4
5
6
7
<suki:SukiSideMenu ItemsSource="{Binding Menus}">
	<suki:SukiSideMenu.ItemTemplate>
		<DataTemplate>
			<TextBlock Text="111"/>
		</DataTemplate>
	</suki:SukiSideMenu.ItemTemplate>
</suki:SukiSideMenu>

运行!

成功报错

不猜了,看看 Demo 里面是怎么写的:

1
2
3
4
5
6
7
8
9
10
11
<suki:SukiSideMenu IsSearchEnabled="True" ItemsSource="{Binding DemoPages}" SelectedItem="{Binding ActivePage}">
    <suki:SukiSideMenu.ItemTemplate>
        <DataTemplate>
            <suki:SukiSideMenuItem Classes="Compact" Header="{Binding DisplayName}">
                <suki:SukiSideMenuItem.Icon>
                    <avalonia:MaterialIcon Kind="{Binding Icon}" />
                </suki:SukiSideMenuItem.Icon>
            </suki:SukiSideMenuItem>
        </DataTemplate>
    </suki:SukiSideMenu.ItemTemplate>
</suki:SukiSideMenu>

行吧,ItemTemplate 直接塞一个 SukiSideMenuItem 就好了,这下菜单项是正常显示出来了。

0x02 显示内容

SukiSideMenu 是把菜单页面直接放在 SukiSideMenuItem 里的,我猜这应该是为了实现动画比较方便吧,可以理解,就是调试不太友好,逻辑树直接废掉了。

不说废话,现在我的小工具的状态是,菜单出来了,但是内容没有显示,一直显示的都只是标题,尝试设置 SukiSideMenuItem.ContentTemplate 也是完全没有动静。

感觉这框架有点不按套路出牌。

继续翻 Demo 代码,貌似也没什么特别的,调试发现他是用 ViewLocator 实现View的,行吧,可以理解。于是现在代码结构变成了这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
	<suki:SukiSideMenu ItemsSource="{Binding Menus}">
		<suki:SukiSideMenu.ItemTemplate>
			<DataTemplate>
				<suki:SukiSideMenuItem Header="{Binding Name}">
					<suki:SukiSideMenuItem.Icon>
						<mIcon:MaterialIcon Kind="{Binding IconKind}" Width="24" Height="24" />
					</suki:SukiSideMenuItem.Icon>
				</suki:SukiSideMenuItem>
			</DataTemplate>
		</suki:SukiSideMenu.ItemTemplate>
		<suki:SukiSideMenu.DataTemplates>
			<DataTemplate DataType="vm:MenuItemViewModel">
				<rxui:ViewModelViewHost ViewModel="{Binding ViewModel}">
					<rxui:ViewModelViewHost.ViewLocator>
						<c:AppViewLocator CacheView="True"/>
					</rxui:ViewModelViewHost.ViewLocator>
				</rxui:ViewModelViewHost>
			</DataTemplate>
		</suki:SukiSideMenu.DataTemplates>
	</suki:SukiSideMenu>

内容成功显示了出来。

这样就可以了吗?很遗憾依然不行,虽然内容是显示了,但是切换菜单的时候依然会报错。原因是我的每一个菜单的View 都是缓存下来的,但是这个 ViewModelViewHost 每次切换都会换新的,这就导致第二次选中某个菜单时,菜单已经是在视觉树上了,但是又重新尝试挂到视觉树上导致报错。没得办法,只好给 MenuItemViewModel 也配一个 View 让他也一起缓存下来。

1
2
3
4
5
6
7
8
9
<UserControl
    .. 这里省略一些代码
    >
	<rxui:ViewModelViewHost ViewModel="{Binding ViewModel}">
		<rxui:ViewModelViewHost.ViewLocator>
			<c:AppViewLocator CacheView="True"/>
		</rxui:ViewModelViewHost.ViewLocator>
	</rxui:ViewModelViewHost>
</UserControl>

最后,附带上带有缓存的 ViewLocator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    public class ViewLocator : IDataTemplate
    {
        private Dictionary<object, object?> Cache = new();

        public Control Build(object? data)
        {
            if (data is null)
                return new TextBlock() { Text = "Data is null" };

            if (!Cache.ContainsKey(data))
                Cache.Add(data, ReactiveUI.ViewLocator.Current.ResolveView(data, null));

            return (Control)Cache[data]!;
        }

        public bool Match(object? data) => data is ViewModelBase;
    }
本文由作者按照 CC BY 4.0 进行授权