Windows 10 UWP 46 of N: MSIX 應用程式封裝工具 Part 4 - MSIX with Extension/Adds-on ( Aka 延伸套件 )

介紹如何使用 MSIX的檔案格式來建立 Extension ( Adds-on)的實際流程,如何讓程式更加模組化的設計喔~

前言

這篇來介紹實際如何把 MSIX 也能有跟 UWP一樣的 Extension( Adds-on )的功能,先前有一篇寫到Xbox Gamebar extension (這篇) 其實就是類似這樣的概念,而在更之前也有寫過 UWP 版本的Extension介紹 (這篇)。這邊我用 .NET 5.0以及WPF作為Sample~


實際操作

開發環境

  • Windows 10 latest
  • Visual Studio 2019
    • Workload 選項
      • UWP workload
      • .NET desktop workload
  • MSIX Packaging Tool
這次會一次使用上 MSIX Packaging Tool 以及 Visual Studio 2019,原因後續會說明。

本來在 .NET 的生態系中就有所謂的 Extension 的概念~由 DllLoad的方式載入不同的實作功能來達到。開啟 Visual Studio 2019後建立兩個 .NET Standard 的專案,然後再建立 .NET 5的WPF應用程式和Windows Application Packaging專案

SampleCSharpApp.Share 裡面只放 Interface,而SampleCSharpApp.Extension1只放實作。然後 SampleCSharp.App是 WPF的專案,最後SampleCSharp.Pack則是Windows Application Packaging 的專案。然後我這邊設計了個簡單的介面

提供擴充的介面 IExtension.cs

	public interface IExtension
	{
		string Name { get; }

		Task<bool> ExecuteAsync(string message, string filePath);
	}

然後實作的 SystemFileExtension.cs

public class SystemFileExtension : IExtension
	{
		public string Name => "HelloWorldExtension";

		public async Task<bool> ExecuteAsync(string message, string filePath)
		{
			if (string.IsNullOrEmpty(message) || string.IsNullOrWhiteSpace(message))
			{
				throw new ArgumentNullException(nameof(message));
			}

			if (string.IsNullOrEmpty(filePath) || string.IsNullOrWhiteSpace(filePath))
			{
				throw new ArgumentNullException(nameof(filePath));
			}

			try
			{
				if (File.Exists(filePath))
				{
					File.Delete(filePath);
				}

				var messageBytes = Encoding.UTF8.GetBytes(message);
				using var fileStream = File.Create(filePath);
				await fileStream.WriteAsync(messageBytes, 0, messageBytes.Length);
				await fileStream.FlushAsync();
				return true;
			}
			catch (Exception ex)
			{
				System.Diagnostics.Debug.WriteLine(ex.ToString());
				throw;
			}
		}
	}

這邊是個簡單地把 string寫到 file的程式~

然後 WPF的主程式不直接加入 Extension的專案因為我們要設計成能夠掛載 Extension 的架構。

接著我在 WPF專案寫了個簡單的MVVM,有些細節就別怪我懶惰拉~這邊有提到我用 WPF和 .NET 5這邊我調整了一下 csproj  專案檔的結構如下

<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
	<PropertyGroup>
		<OutputType>WinExe</OutputType>
		<TargetFramework>net5.0-windows10.0.19041.0</TargetFramework>
		<LangVersion>latest</LangVersion>
		<RuntimeIdentifiers>win10-x64;win10-x86</RuntimeIdentifiers>
		<Platforms>x64;x86</Platforms>
		<UseWPF>true</UseWPF>
	</PropertyGroup>

	<ItemGroup>
		<ProjectReference Include="..\SampleCSharpApp.Share\SampleCSharpApp.Share.csproj" />
	</ItemGroup>
</Project>

MainWindow.xaml

<Window
    x:Class="SampleCSharp.App.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:SampleCSharp.App"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">

    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <Grid x:Name="LayoutRoot">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <StackPanel>
            <Button Command="{Binding GetExtensiosCommand}" Content="Refresh extension list" />
        </StackPanel>
        <ListView Grid.Row="1" ItemsSource="{Binding Extensions}">
            <ListView.ItemTemplate>
                <DataTemplate>
                    <DockPanel>
                        <TextBlock Text="{Binding Name}" />
                    </DockPanel>
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
    </Grid>
</Window>

MainWindowViewModel.cs

using Windows.ApplicationModel;

public class MainWindowViewModel : BinableBase
	{
		public IList<SampleCSharpApp.Share.IExtension> Extensions { get; }

		public ICommand GetExtensiosCommand { get; }

		public MainWindowViewModel()
		{
			Extensions = new ObservableCollection<SampleCSharpApp.Share.IExtension>();

			GetExtensiosCommand = new RelayCommand(async () => await GetExtensionsAsync());
		}

		private async Task GetExtensionsAsync()
		{
			if (!Package.Current.Dependencies.Any())
			{
				return;
			}

			foreach (var dependencyin Package.Current.Dependencies)
			{
				if (dependency.Id.Name.Contains("Extension") && dependency.IsOptional )
				{
					var file = await dependency.InstalledLocation.GetFileAsync("SampleCSharpApp.Extension1.dll");
					LoadAssembly(file.Path);
				}
			}
		}

		private void LoadAssembly(string filePath)
		{
			var loadedAssembly = Assembly.LoadFile(System.IO.Path.GetFullPath(filePath));
			foreach (var innerType in loadedAssembly.GetTypes().Where(i => i.IsClass))
			{
				if (innerType.GetInterfaces().Contains(typeof(SampleCSharpApp.Share.IExtension)))
				{
					Extensions.Add((SampleCSharpApp.Share.IExtension)Activator.CreateInstance(innerType));
				}
			}
		}
	}
這邊有我使用了 Windows Runtime的API 就是可以從 Package的結構(也就是MSIX的封裝)取得屬於相依在這個MSIX檔的其他MSIX檔,也就是在 GetExtensionsAsync內然後再丟給 LoadAssembly去做 .NET 透過 Reflection 來做到把套件載入到主要程式中。 說破了就是強大的 Reflection來把 DLL內的型別做成物件~

接下來可以用 SampleCSharp.Pack的 Windows Application Packaging project 來發佈打包成 MSIX檔案~這邊看一下主程式的 Package.appxmanifest

這邊要特別注意的就是 Identity內的 Name就是這個MSIX的ID喔!要記起來等等會用到~

然後這邊就準備要把 Extension的DLL進行MSIX的封裝了~這邊不用 Windows Application Packaging Project 因為該專案類型一定要有 Exe 作為起始的專案!所以這個就不符合我們想要做成MSIX的流程,這時候就是使用 MSIX Packaging Tool了~使用 MSIX Packaging tool的流程請參考MSIX系列的第一篇。重點就是把 Extension編譯好的DLL們打包進去!

我們這邊用 MSIX Packaging Tool來看一下結構

基本上就是把 DLL放進去,而 Assets只是MSIX的相關圖片檔案並沒有跟我們程式碼有關的檔案。

接著重點就是要調整Extension這包的 Manifest了喔!請看下方Manifest xml

<?xml version="1.0" encoding="utf-8"?>
<Package 
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" 
xmlns:uap4="http://schemas.microsoft.com/appx/manifest/uap/windows10/4" 
IgnorableNamespaces="uap4">
  <!--Package created by MSIX Packaging Tool version: 1.2020.1219.0-->
  <Identity Name="Extension" Publisher="CN=Richie" Version="1.0.0.0" ProcessorArchitecture="x64" />
  <Properties>
    <DisplayName>SampleCSharp Extension</DisplayName>
    <PublisherDisplayName>DevRichie</PublisherDisplayName>
    <Description>None</Description>
    <Logo>Assets\StoreLogo.png</Logo>
  </Properties>
  <Resources>
    <Resource Language="en-us" />
  </Resources>
  <Dependencies>
    <TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.1" />
    <uap4:MainPackageDependency Name="b412cb35-056d-4d25-a1a1-a033489217c5" />
  </Dependencies>
</Package>
重點就是在 xmlns加入 uap4的宣告並且在 Dependencies裡面加入 uap4:MainPackageDepedency的元素並把 Name屬性放入主程式MSIX內的ID,然後還要注意的是把Capabilities要拿掉。因為套件所使用的權限會跟著主程式的 Manifest宣告

然後把兩個 MSIX都安裝起來後來看看使否正常,進到 Settings的應用程式列表看看

點開 進階選項 就可以看到 

打包好的 adds-on的列表就會出現拉~

這邊就是程式執行起來的樣子

然後按下 Refresh extension list的 Button就會出現拉~


總結

使用與UWP相似的架構概念可以讓Win32的安全性更加提升,也讓整體Windows App享有從封裝層就有Extension的概念並統一UWP或是MSIX Win32可以更加一致的使用新版Settings的相關功能。

 

***以上Code以及說明都有可能隨著Windows 10 的版本以及Visual Studio 版本有所調整!***

參考資料 Microsoft Docs,Windows Blogs for developer

下次再分享Windows 10 的新技術拉~