65.9K
CodeProject 正在变化。 阅读更多。
Home

VB 的缺失 Avalonia 模板

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.86/5 (9投票s)

2023年3月21日

CPOL

9分钟阅读

viewsIcon

16441

downloadIcon

232

添加了 App 和 Control Library 项目缺失的项目模板,这些模板未包含在内。

目录

引言

我正在撰写一篇新文章,其中包含 C#VB 代码示例。如果您想了解 VB 使用 Avalonia 的实际应用,请查看下一篇文章:WinForms、WPF 和 Avalonia 中的 LogViewer 控件 (C# & VB)[^]

我以前从未接触过 Avalonia,惊讶地发现它只正式支持 C#F# 应用程序模板。我在 Visual Studio Marketplace 上搜索了一下,没有找到 VB 的模板。于是我快速搜索了一下,发现了一个部分完成的 VB 模板:avalonia-vb-template-app | Github。我们需要自己实现一个。

当您添加 Visual Studio 2022 的 Avalonia 扩展时,会添加三个模板

什么是 Avalonia?

来自 Avalonia 官方 - 工作原理 页面

引用

Avalonia UI 使 .NET 开发人员能够从单个代码库为桌面、移动和 Web 创建像素完美的应用程序。Avalonia UI 从 WPF 和 WinUI 中汲取灵感,其目标从未仅仅是复制 WPF 或 WinUI API。

支持的操作系统

  1. 桌面
  • Windows 7 (32 & 64位), 10 (32 & 64位), 11 (64位)
  • MacOS/OSX (Sierra 10.13 及更高版本)
  • Linux Ubuntu 16.04+, Debian 9+, Fedora 30+ (Arm, Arm64, x64)
  • Unix
  1. 移动
  • iOS
  • Android
  1. 物联网
  • Raspberry Pi
  1. 通过 WASM 的浏览器

支持的 IDE

  • Visual Studio 2017
  • JetBrains Rider 2020.3

欲了解更多信息,请参阅 Avalonia UI 文档(官方网站)

概述

本文仅针对 桌面开发。所有代码都包含在本文开头的下载链接中。下载链接中包含的示例项目涵盖了官方默认的 C# 模板和我们新的最小 VB 项目模板。

重点将放在弥补 VB 支持的不足上,提供完整的基项目,包括:

  • Avalonia 应用程序
  • Avalonia MVVM 社区工具包应用程序
  • Avalonia MVVM ReactiveUI 应用程序

作为一个额外的福利,在默认模板中没有找到的

  • Avalonia 控件库

我将介绍我是如何将 C# 模板转换为 VB 项目的。如果您想为创建新项目添加自己的模板,下面有一个名为 Visual Studio 操作指南:将示例项目制作成可重用模板 的部分,其中展示了具体操作。

示例截图

在深入文章之前,让我们看看我们要实现什么,一个 VB 的最小模板。下面是同一个项目在两个不同的操作系统上编译并运行的情况。

Windows 11

MacOS Monterey (v12.6.3)

C# Avalonia 项目模板是什么样的?

我们将查看上面截图中的第一个模板,Avalonia .NetCore App (AvaloniaUI),因为它对所有 Avalonia 项目模板都是通用的。

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
    <ApplicationManifest>app.manifest</ApplicationManifest>
  </PropertyGroup>

  <ItemGroup>
    <TrimmerRootAssembly Include="Avalonia.Themes.Fluent" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Avalonia" Version="0.10.18" />
    <PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
    <!--Condition below is needed to remove Avalonia.Diagnostics 
        package from build output in Release configuration.-->
    <PackageReference Condition="'$(Configuration)' == 'Debug'" 
     Include="Avalonia.Diagnostics" Version="0.10.18" />
    <PackageReference Include="XamlNameReferenceGenerator" Version="1.6.1" />
  </ItemGroup>
</Project>

这看起来像一个普通的控制台应用程序项目文件,带有必需库文件的 `PackageReference` 和设置为 `WinExe` 的 `OutputType`。`WinExe` 只是意味着隐藏控制台窗口。

作为比较,以下是默认的 控制台应用程序 项目模板的 * .csproj 文件

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

最后两个元素 `ImplicitUsings` 和 `Nullable` 是 C# 特有的,对 VB 而言不是必需的。

我们如何为 VB 创建一个 Avalonia 应用程序项目?

我们需要手动构建项目文件和相关文件来引导 Avalonia 框架。

项目文件

我们来看看一个默认的控制台应用程序 * .vbproj 文件

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <RootNamespace>ConsoleApp2</RootNamespace>
    <TargetFramework>net7.0</TargetFramework>
  </PropertyGroup>

</Project>

C# 和 VB 项目文件的主要区别在于 VB 项目文件有一个 `RootNamespace` 元素。

因此,对于 Avalonia,我们可以添加缺失的引用

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <Nullable>enable</Nullable>
    <BuiltInComInteropSupport>true</BuiltInComInteropSupport>
    <ApplicationManifest>app.manifest</ApplicationManifest>
    <RootNamespace>AvaloniaApplicationVB</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <TrimmerRootAssembly Include="Avalonia.Themes.Fluent" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Avalonia" Version="0.10.18" />
    <PackageReference Include="Avalonia.Desktop" Version="0.10.18" />
    <!--Condition below is needed to remove Avalonia.Diagnostics 
        package from build output in Release configuration.-->
    <PackageReference Condition="'$(Configuration)' == 'Debug'" 
     Include="Avalonia.Diagnostics" Version="0.10.18" />
    <PackageReference Include="XamlNameReferenceGenerator" Version="1.6.1" />
  </ItemGroup>

</Project>

现在我们有了一个基本的 Avalonia 项目。接下来,我们需要连接引导代码。

已知问题

在转换 C# 代码之前,我们需要快速查看撰写本文时(v0.10.18)的已知问题

Avalonia 的设计考虑了 C#F#,而非 VB。这并不能阻止我们使用 VBAvalonia 创建应用程序,然而,使用 VB 需要更多的工作

  • 文件嵌套无效
  • 没有用于 Windows 和用户控件的项模板
  • 虽然设计器可以进行可视化操作,但没有 `*.g.vb` `code-gen` 将控件连接到代码后台
    • 通过设计器添加事件将在后台代码中创建方法
    • 指定控件名称不会自动生成 `*.g.vb` 文件以将控件链接到后台代码。下面将介绍如何解决此问题。

转换应用程序启动代码

最小的 必需 启动是两个(带后台代码则为三个)文件:`Program` (.vb) 和 `App` (.axaml & * .axaml.vb)。为简洁起见,我将只关注转换后的 VB 代码,默认生成的 C# 代码包含在下载中。

Program.VB 文件

Imports Avalonia

Module Program

    <STAThread>
    Sub Main(args As String())

        BuildAvaloniaApp() _
            .StartWithClassicDesktopLifetime(args)

    End Sub

    Public Function BuildAvaloniaApp() As AppBuilder

        Return AppBuilder.Configure(Of App) _
            .UsePlatformDetect() _
            .LogToTrace()

    End Function

End Module

App (.axaml & .axaml.vb) 文件

<Application x:Class="AvaloniaApplicationVB.App"
             xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <Application.Styles>
        <FluentTheme Mode="Light"/>
    </Application.Styles>
</Application>

以及后台代码

Imports Avalonia
Imports Avalonia.Controls.ApplicationLifetimes
Imports Avalonia.Markup.Xaml

Public Partial Class App
    Inherits Application

    Public Overrides Sub Initialize()
        AvaloniaXamlLoader.Load(Me)
    End Sub

    Public Overrides Sub OnFrameworkInitializationCompleted()
        Dim desktop As IClassicDesktopStyleApplicationLifetime = Nothing

        desktop = TryCast(ApplicationLifetime, IClassicDesktopStyleApplicationLifetime)
        if desktop IsNot Nothing Then
            desktop.MainWindow = New MainWindow()
        End If

        MyBase.OnFrameworkInitializationCompleted()
    End Sub

End Class

MainWindow (.axaml & .axaml.vb) 文件

<Window x:Class="AvaloniaApplicationVB.MainWindow"
        xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        x:Name="Window"

        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        
        Title="AvaloniaApplicationVB"
        Width="800" Height="450">

    Welcome to Avalonia VB!

</Window>

以及后台代码

Imports Avalonia.Controls
Imports Avalonia.Markup.Xaml

Partial Public Class MainWindow : Inherits Window

    Private Window As Window

    Sub New()

        ' This call is required by the designer.
        InitializeComponent()

    End Sub

    ' Auto-wiring does not work for VB, so do it manually
    ' Wires up the controls and optionally loads XAML markup and attaches dev tools
    ' (if Avalonia.Diagnostics package is referenced)
    Private Sub InitializeComponent(Optional loadXaml As Boolean = True)

        If loadXaml Then
            AvaloniaXamlLoader.Load(Me)
        End If

        ' An example of manually getting the named Window
        Window = FindNameScope().Find("Window")

    End Sub

End Class
我们是如何知道使用 FindNameScope 方法的?

通过在 * .csproj 项目文件中打开 `true` 标志,我们可以在 `obj\Debug\net7.0\generated\Avalonia.NameGenerator\Avalonia.NameGenerator.AvaloniaNameSourceGenerator` 路径中查看生成的 `AvaloniaApplication.MainWindow.g.cs` 文件

// <auto-generated />

using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace AvaloniaApplication
{
    partial class MainWindow
    {
        internal global::Avalonia.Controls.Window MyWindow;

        /// <summary>
        /// Wires up the controls and optionally loads XAML markup 
        /// and attaches dev tools
        /// (if Avalonia.Diagnostics package is referenced).
        /// </summary>
        /// <param name="loadXaml">Should the XAML be loaded into the component.
        /// </param>
        /// <param name="attachDevTools">Should the dev tools be attached.</param>

        public void InitializeComponent
               (bool loadXaml = true, bool attachDevTools = true)
        {
            if (loadXaml)
            {
                AvaloniaXamlLoader.Load(this);
            }

#if DEBUG
            if (attachDevTools)
            {
                this.AttachDevTools();
            }
#endif

            MyWindow = this.FindNameScope()?.Find
                       <global::Avalonia.Controls.Window>("MyWindow");
        }
    }
}

在 AXAML 后台代码中手动连接控件引用

如果我们给一个控件命名,并且需要在后台代码中引用该控件,我们需要手动完成

Dim Control_Reference = FindNameScope().Find("Control_Name")

例如,如果我们在 AXAML 文件中有以下内容

<TextBlock x:Name="MyTextBlock"/>

在后台代码中,我们将执行以下操作

Dim MyTextBlock = FindNameScope().Find("MyTextBlock")

' Then we can set properties, call the control's methods, or wire up event handlers
' For example:
MyTextBlock.Text = "Hello World from the Code-Behind!"

MVVM 应用程序项目

开箱即用,对于 C#F#Avalonia 模板有两种 (2) 类型的 MVVM 项目类型

  1. 社区工具包
  2. ReactiveUI

项目文件

与之前的版本相同,除了库引用。

社区工具包
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.1.0" />
ReactiveUI
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.18" />

Program.VB 文件(社区工具包)

这与之前的项目类型相同。

Program.VB 文件 (ReactiveUI)

对于 ReactiveUI,我们需要引入支持

Imports Avalonia
Imports Avalonia.ReactiveUI

Module Program

    <STAThread>
    Sub Main(args As String())

        BuildAvaloniaApp() _
            .StartWithClassicDesktopLifetime(args)

    End Sub

    Public Function BuildAvaloniaApp() As AppBuilder

        Return AppBuilder.Configure(Of App) _
            .UsePlatformDetect() _
            .LogToTrace() _
            .UseReactiveUI()

    End Function

End Module

ViewLocator

默认情况下,Avalonia 为数据模板实现了 `ViewLocator` 设计模式的支持。本文附带的示例项目已实现了这一点。

Imports Avalonia.Controls
Imports Avalonia.Controls.Templates
Imports AvaloniaMvvmApplicationReactiveUIVB.ViewModels

Public Class ViewLocator : Implements IDataTemplate

    Public Function Build(data As Object) As IControl _
                    Implements ITemplate(Of Object, IControl).Build

        Dim name As String = data.GetType().FullName.Replace("ViewModel", "View")
        Dim type As Type = Type.GetType(name)

        If type IsNot Nothing Then
            Return DirectCast(Activator.CreateInstance(type), Control)
        End If

        Return New TextBox With {.Text = "Not Found: " + name}

    End Function

    Public Function Match(data As Object) As Boolean Implements IDataTemplate.Match

        Return TypeOf data Is ViewModelBase

    End Function

End Class

App (.axaml & .axaml.vb) 文件

这些文件对于 Community ToolkitReactiveUI 都是相同的。我们需要添加一个对 `ViewLocator` 的引用

<Application x:Class="AvaloniaMvvmApplicationCommunityToolkitVB.App"

             xmlns:local="using:AvaloniaMvvmApplicationCommunityToolkitVB"

             xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

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

    <Application.Styles>
        <FluentTheme Mode="Light"/>
    </Application.Styles>
</Application>

以及后台代码

Imports Avalonia
Imports Avalonia.Controls.ApplicationLifetimes
Imports Avalonia.Data.Core
Imports Avalonia.Data.Core.Plugins
Imports Avalonia.Markup.Xaml
Imports AvaloniaMvvmApplicationCommunityToolkitVB.ViewModels

Partial Public Class App
    Inherits Application

    Public Overrides Sub Initialize()
        AvaloniaXamlLoader.Load(Me)
    End Sub

    Public Overrides Sub OnFrameworkInitializationCompleted()
        Dim desktop As IClassicDesktopStyleApplicationLifetime = Nothing

        desktop = TryCast(ApplicationLifetime, IClassicDesktopStyleApplicationLifetime)
        If desktop IsNot Nothing Then

            ' Line below is needed to remove Avalonia data validation.
            ' Without this line you will get duplicate validations 
            ' from both Avalonia and CT
            ExpressionObserver.DataValidators _
                .RemoveAll(Function(x) TypeOf x Is DataAnnotationsValidationPlugin)

            desktop.MainWindow = New MainWindow() With
            {
                .DataContext = New MainWindowViewModel()
            }

        End If

        MyBase.OnFrameworkInitializationCompleted()

    End Sub

End Class 

MainWindow (.axaml & .axaml.vb) 文件

这些文件对于 Community ToolkitReactiveUI 都是相同的

<Window x:Class="AvaloniaMvvmVB.MainWindow"
        xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:vm="using:AvaloniaMvvmVB.ViewModels"
        x:DataType="vm:MainWindowViewModel"

        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        
        Title="AvaloniaMvvmVB"
        Icon="/Assets/avalonia-logo.ico"
        Width="800" Height="450">

    <Design.DataContext>
        <!-- This only sets the DataContext for the previewer in an IDE,
             to set the actual DataContext for runtime, 
             set the DataContext property in code (look at App.axaml.cs) -->
        <vm:MainWindowViewModel/>
    </Design.DataContext>

    <TextBlock Text="{Binding Greeting}" HorizontalAlignment="Center" 
     VerticalAlignment="Center"/>

</Window>

以及后台代码

Imports Avalonia.Controls
Imports Avalonia.Markup.Xaml
Imports AvaloniaMvvmVB.ViewModels

Partial Public Class MainWindow : Inherits Window

    Private VM As MainWindowViewModel

    Sub New()

        ' This call is required by the designer.
        InitializeComponent()

    End Sub

    ' Auto-wiring does not work for VB, so do it manually
    ' Wires up the controls and optionally loads XAML markup 
    ' and attaches dev tools (if Avalonia.Diagnostics package is referenced)
    Private Sub InitializeComponent(Optional loadXaml As Boolean = True)

        If loadXaml Then
            AvaloniaXamlLoader.Load(Me)
        End If

    End Sub

    ' An example of capturing the ViewModel if required in the code-behind
    Protected Overrides Sub OnDataContextChanged(e As EventArgs)

        VM = DataContext
        MyBase.OnDataContextChanged(e)

    End Sub

End Class 

ViewModelBase

社区工具包ReactiveUI 使用不同的方法来实现数据绑定支持,因此基本实现略有不同

社区工具包
Imports CommunityToolkit.Mvvm.ComponentModel

Namespace ViewModels

    Public Class ViewModelBase : Inherits ObservableObject

    End Class

End Namespace 
ReactiveUI
Imports ReactiveUI

Namespace ViewModels

    Public Class ViewModelBase : Inherits ReactiveObject

    End Class

End Namespace 

MainWindowViewModel

此文件对于 Community ToolkitReactiveUI 均相同。

Namespace ViewModels

    Public Class MainWindowViewModel : Inherits ViewModelBase

        Public ReadOnly Property Greeting As String _
            = "Welcome to Avalonia MVVM using VB!"

    End Class

End Namespace 

我们如何为 VB 创建一个 Avalonia 控件库?

在添加 Avalonia 扩展时,或者在官方文档中,都没有安装用于 C#F# 的 UserControl 或自定义控件库模板。如果您使用过 WPF 的同等功能,您就会知道这并不困难。下面,我将引导您创建一个最小的实现。

*.vbproj 项目文件

对于 `UserControl` 或自定义控件库,项目文件与应用程序项目非常相似

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
    <RootNamespace>AvaloniaControlLibraryVB</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Avalonia" Version="0.10.18" />
    <!--Condition below is needed to remove Avalonia.Diagnostics 
        package from build output in Release configuration.-->
    <PackageReference Condition="'$(Configuration)' == 'Debug'" 
     Include="Avalonia.Diagnostics" Version="0.10.18" />
    <PackageReference Include="XamlNameReferenceGenerator" Version="1.6.1" />
  </ItemGroup>

</Project> 

UserControl (.axaml & .axaml.vb) 文件

与任何 WPF 用户控件一样,Avalonia 的实现非常相似。但是,如果您需要从后台代码中引用 AXAML 中的控件,则需要使用 `FindNameScope` 方法手动引用控件,就像我们上面在 `MainWindow` 中所做的那样。

<UserControl x:Class="AvaloniaControlLibraryVB.UserControl1"
             xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

             mc:Ignorable="d"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008">
    Welcome to Avalonia Control Library VB!
</UserControl> 

以及后台代码

Partial Public Class UserControl1 : Inherits UserControl

    Sub New()
        InitializeComponent()
    End Sub

    ' Auto-wiring does not work for VB, so do it manually
    ' Wires up the controls and optionally loads XAML markup and 
    ' attaches dev tools (if Avalonia.Diagnostics package is referenced)
    Private Sub InitializeComponent(Optional loadXaml As Boolean = True)

        If loadXaml Then
            AvaloniaXamlLoader.Load(Me)
        End If

    End Sub

End Class 

在控件库中使用控件与 WPF 完全相同——创建一个命名空间,然后使用带有命名空间引用的控件

<Window x:Class="SampleControlUseAppVB.MainWindow"
        xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

        x:Name="Window"

        mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        
        xmlns:controls="clr-namespace:AvaloniaControlLibraryVB;
                        assembly=AvaloniaControlLibraryVB"

        Title="SampleControlUseAppVB"
        Width="800" Height="450">

    <controls:UserControl1 />

</Window> 

Visual Studio 操作指南:将示例项目制作成可重用模板

Visual Studio 使从项目创建自己的模板变得非常容易。您可以在此处阅读相关内容:操作方法:创建项目模板 | Microsoft Learn

要访问 Visual Studio 中的 导出模板向导,请加载包含要从中创建模板的解决方案,然后从菜单中选择 导出模板向导项目 > 导出模板

  1. 选择“项目模板”选项,然后选择要导出的项目

  2. 填写模板详细信息,是否要自动导入生成的模板,以及是否要打开资源管理器窗口到生成的模板文件夹

大功告成!现在您可以在 Visual Studio 中将您的项目用作模板。

最佳方法是手动执行此过程,然后您可以正确标记项目以进行“新建项目”筛选。这是一个视频,将引导您完成该过程:如何在 .NET 中创建自己的项目模板 | YouTube

摘要

尽管 VB 没有得到官方支持,但我们已经了解到,您可以使用 VB 为 Avalonia 创建跨平台应用程序和库。我还向您介绍了将项目从 C# 转换为 VB 的过程,以及如何为 Avalonia 和任何其他您希望在新项目中使用可重用样板代码的项目创建自己的项目模板。

参考文献

文档、文章等

Nuget 包

历史

  • 2023年3月21日 - v1.00 - 初次发布
  • 2023年3月26日 - v1.01 - 移除用于构建本文的临时文件残留
© . All rights reserved.