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

使用 Electron.NET 构建跨平台桌面应用

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2020年9月15日

CPOL

11分钟阅读

viewsIcon

33353

本文提供了使用 Electron.NET 开发和部署应用程序的逐步指南。

最近,我很荣幸在 NDC 奥斯陆在线活动中介绍了这个主题。这是我的博客形式的演讲,文章末尾附有源代码链接。

什么是 Electron?

Electron 是一个框架,支持使用 Chromium 渲染引擎和 Node.js 运行时等 Web 技术开发桌面应用程序。支持的操作系统包括 Windows、macOS 和 Linux。您很可能使用过以下至少一个使用 Electron 开发的应用程序:

  • Visual Studio Code
  • Slack
  • Discord
  • Skype
  • GitHub Desktop
  • Twitch

Electron 利用 HTML、CSS 和 JavaScript 等熟悉的标准。但是,如果您是一位习惯于使用 C# 的 .NET 开发人员怎么办?这就是 Electron.NET 发挥作用的地方。

什么是 Electron.NET?

Electron.NET 是一个包装器,它封装了一个带有嵌入式 ASP.NET Core 应用程序的“普通”Electron 应用程序。它是一个开源项目,允许 .NET 开发人员使用 C# 调用本机 Electron API。Electron.NET 由两个组件组成:

  1. 一个 NuGet 包,将 Electron API 添加到 ASP.NET Core 项目中
  2. 一个 .NET Core 命令行扩展,用于为 Windows、macOS 和 Linux 平台构建和启动应用程序。

Electron.NET 需要事先安装以下软件:

我依靠 Electron.NET 开发了 C1DataEngine Workbench,这是一个跨平台工具,支持创建和可视化由适用于 .NET Standard 的 ComponentOne DataEngine 库管理的数据分析工作区。我最初计划将其作为一个标准 Electron 应用程序,通过调用 .NET Core 全局工具的 shell 命令与库通信。但当我发现 Electron.NET 后,我能够消除 shell 命令并直接调用库。

Building Cross-Platform Desktop Apps with Electron.NET

创建一个 ASP.NET Core Web 应用程序

在本练习中,我使用的是在 Mac 上运行的 Visual Studio Code。首先,打开一个终端窗口并运行以下命令创建一个名为 Processes 的新项目。

mkdir Processes  
cd Processes  
dotnet new webapp  
code .  

当 Visual Studio Code 提示时,选择 加载项目所需的资产。按 F5 构建并运行应用程序,在 localhost:5001 上打开默认的 ASP.NET Core 欢迎页面。关闭页面,返回 VS Code 并停止调试。

Electron 化!

现在让我们将我们的样板 ASP.NET Core 项目转换为 Electron 应用程序。此步骤涉及向项目文件添加 NuGet 包,插入一些初始化代码,以及安装一个命令行工具来执行构建。首先,打开文件 Processes.csproj 并插入对托管在 nuget.org 上的 Electron.NET API 的包引用:

<ItemGroup>  
  <PackageReference Include="ElectronNET.API" Version="9.31.2" />  
</ItemGroup>  

保存文件,然后在 VS Code 提示时恢复包。此操作可让您立即访问 Intellisense,以便后续修改代码。

接下来,编辑 Program.cs 并为新添加的包插入一个 using 语句:

using ElectronNET.API;  

找到静态方法 CreateHostBuilder 并在调用 UseStartup 之前插入以下两行:

webBuilder.UseElectron(args);  
webBuilder.UseEnvironment("Development");  

第一行是必需的。第二行在开发过程中很方便,因为它允许显示详细的错误消息。

编辑 Startup.cs 并插入以下 using 语句:

using ElectronNET.API;  
using ElectronNET.API.Entities;  
using System.Runtime.InteropServices; 

找到 Configure 方法并将其主体末尾添加以下行:

if (HybridSupport.IsElectronActive)  
{  
    CreateWindow();  
}  

最后,将以下方法添加到 Startup 类中以创建主 Electron 窗口:

private async void CreateWindow()  
{  
    var window = await Electron.WindowManager.CreateWindowAsync();  
    window.OnClosed += () => {  
        Electron.App.Quit();  
    };  
}

由于我们的应用程序由一个窗口组成,我们处理 OnClosed 事件以在用户关闭窗口(而不是从主菜单中选择“退出”)时终止应用程序。

安装命令行工具

除了您之前在项目文件中引用的运行时包,Electron.NET 还提供了一个命令行工具来执行构建和部署任务。在 VS Code 中,创建一个新的终端窗口并输入:

dotnet tool install ElectronNET.CLI -g  

这个一次性步骤将安装一个 .NET Core 全局工具,该工具实现了一个名为 electronize 的命令。要查看系统上安装的工具/命令列表,请键入以下内容:

dotnet tool list -g

运行 Electron 应用程序

安装命令行工具后,在 VS Code 终端窗口中键入以下行:

electronize init  
electronize start

第一行是一次性步骤,它会创建一个名为 electron.manifest.json 的清单文件并将其添加到您的项目中。第二行用于启动 Electron 应用程序(不要使用 F5,因为这只会浏览器中打开 ASP.NET Core 应用程序)。请注意,内容现在显示在应用程序窗口中,而不是浏览器中。

Building Cross-Platform Desktop Apps with Electron.NET

另请注意默认的 Electron 应用程序菜单。在 Mac 上,此菜单不是窗口本身的一部分,而是固定在屏幕顶部。

Building Cross-Platform Desktop Apps with Electron.NET

在撰写本文时,您创建项目时安装的 Bootstrap 模块中存在脚本错误。

要查看它,打开“视图”菜单并单击“切换开发人员工具”。

Uncaught TypeError: Cannot read property 'fn' of undefined  
    at util.js:55  
    at bootstrap.bundle.min.js:6  
    at bootstrap.bundle.min.js:6 

幸运的是,有一个简单的解决方法。首先,打开“Electron”菜单并单击“退出 Electron”以关闭开发人员工具和应用程序窗口。在 VS Code 中,打开 Pages/Shared/_Layout.cshtml 并在 Bootstrap 脚本标签之前插入以下行:

<script>window.$ = window.jquery = module.exports;</script>  

保存此更改,然后在终端窗口中键入 electronize start 以重新运行应用程序。打开开发人员工具并注意错误消息已消失。保持应用程序窗口打开以进行下一步。

调试 ASP.NET 代码

由于我们使用外部命令而不是 F5 启动了应用程序,因此我们需要将调试器附加到正在运行的 ASP.NET 进程。在应用程序窗口打开的情况下,转到 VS Code,打开 Pages/Index.cshtml.cs,并在 OnGet 处理程序上设置一个断点。单击活动栏上的“运行”,从下拉控件中选择 .NET Core Attach,然后单击相邻的图标以显示进程列表。在列表中输入应用程序名称 (Processes) 并选择剩余的项。(如果碰巧仍然显示多个进程,请选择 electronWebPort 值最大的那个。)

Building Cross-Platform Desktop Apps with Electron.NET

在应用程序窗口中,单击“视图”菜单上的“重新加载”,将命中断点。继续执行,关闭应用程序窗口,并注意调试器会自动断开连接。

自定义主页

为了说明 Electron.NET 的跨平台功能,让我们用活动系统进程列表替换默认主页内容。稍后,我们将构建一个 Linux 版本并观察该平台上的差异。

首先,打开 Pages/Index.cshtml.cs 并为进程 API 添加以下 using 语句:

using System.Diagnostics;  

接下来,将以下属性声明添加到 IndexModel 类:

public List<Process> Processes { get; set; } 

现在将以下行添加到 OnGet 处理程序的(最初为空的)主体中:

var items = Process.GetProcesses().Where(p => !String.IsNullOrEmpty(p.ProcessName));
Processes = items.ToList();

现在让我们修改相应的 Razor 页面标记以显示进程列表。

打开 Pages/Index.cshtml 并将整个原始 <div> 标签替换为以下行:

<div>  
     <table id="myTable" class="table table-sm table-striped">  
        <thead class="thead-dark">  
            <tr class="d-flex">  
                <th scope="col" class="col-sm-2">Id</th>  
                <th scope="col" class="col-sm-6">Process Name</th>  
                <th scope="col" class="col-sm-4 text-right">Physical Memory</th>  
            </tr>  
        </thead>  
        <tbody>  
            @foreach (var item in Model.Processes)  
            {  
                <tr class="d-flex">  
                    <td class="col-sm-2" scope="row">@item.Id</td>  
                    <td class="col-sm-6">@try{@item.MainModule.ModuleName}catch{@item.ProcessName}</td>  
                    <td class="col-sm-4 text-right">@item.WorkingSet64</td>  
                </tr>  
            }  
        </tbody>  
    </table>  
</div> 

此修改将显示一个命名进程表,其中包含 ID 号、进程名称和为进程分配的物理内存量的列。请注意内联 try/catch 块的使用。在某些平台上,进程名称可能会被截断,因此模块名称是首选,进程名称作为备用值。

Electron.NET 支持监视模式,它会监视您的更改并自动重建和重新启动您的应用程序。要调用监视模式,请运行以下命令:

electronize start /watch  

现在保存您对项目的所有更改。应用程序重新启动后,修改后的主页应该看起来像这样:

Building Cross-Platform Desktop Apps with Electron.NET

添加详细视图

在典型的 ASP.NET Core 应用程序中,列表中的项目包含指向详细信息页面的链接,用户可以在其中更详细地查看项目或修改它。让我们为单个进程创建一个简单的视图。首先,在 Pages 文件夹中添加一个名为 View.cshtml 的新文件,并插入以下标记:

@page  
@model ViewModel  
@{  
    ViewData["Title"] = "Process view";  
}  
<div>  
    <dl class="row">  
        @foreach (var property in @Model.PropertyList.Select(name => typeof(System.Diagnostics.Process).GetProperty(name)))  
        {  
        <dt class="col-sm-4">  
            @property.Name  
        </dt>  
        <dd class="col-sm-8">  
            @property.GetValue(Model.Process)  
        </dd>  
        }  
    </dl>  
</div>  
<div>  
    <hr />  
    <form method="post">  
        <input type="submit" value="Kill Process" class="btn btn-danger" />  
        <a class="btn btn-light" asp-page="./Index">Back to List</a>  
    </form>  
</div>  

接下来,添加一个相应的名为 View.cshtml.cs 的代码隐藏文件,并插入以下代码:

using System.Diagnostics;  
using System.Threading.Tasks;  
using Microsoft.AspNetCore.Mvc;  
using Microsoft.AspNetCore.Mvc.RazorPages;  
using Microsoft.Extensions.Logging;  
using ElectronNET.API;  
using ElectronNET.API.Entities;

namespace Processes.Pages  
{  
    public class ViewModel : PageModel  
    {  
        private readonly ILogger<ViewModel> _logger;

        public ViewModel(ILogger<ViewModel> logger)  
        {  
            _logger = logger;

            PropertyList = new string[] {  
                "Id",  
                "ProcessName",  
                "PriorityClass",  
                "WorkingSet64"  
            };  
        }  

        public Process Process { get; set; }

        public string[] PropertyList { get; set; }

        public void OnGet(int? id)  
        {  
            if (id.HasValue)  
            {  
                Process = Process.GetProcessById(id.Value);  
            }  

            if (Process == null)  
            {  
                NotFound();  
            }  
        }  
    }  
} 

字符串数组 PropertyList 定义了要在详细视图中显示的 Process 对象属性列表。我们没有在页面标记中硬编码这些字符串,而是使用反射在运行时派生属性名称和值。

要将详细视图链接到主页上的单个项目,请编辑 Pages/Index.cshtml 并替换表达式:

@item.Id  

使用此锚标签:

<a asp-page="./View" asp-route-id="@item.Id">@item.Id</a>  

像以前一样运行应用程序,并注意 Id 列包含导航到类似于此页面的超链接:

Building Cross-Platform Desktop Apps with Electron.NET

您可能已经注意到标记包含一个用于 HTTP POST 请求的提交按钮。让我们通过向 ViewModel 类添加以下方法来完成详细信息页面的实现:

public async Task<IActionResult> OnPostAsync(int? id)  
{  
    if (id.HasValue)  
    {  
        Process = Process.GetProcessById(id.Value);  
    }  

    if (Process == null)  
    {  
        return NotFound();  
    }

    Process.Kill();  
    return RedirectToPage("Index");  
}  

虽然这无疑会完成任务,但最好给用户一个考虑并取消操作的机会。让我们用以下代码替换最后两行,该代码使用 Electron.NET 的 ShowMessageBoxAsync API 向用户显示一个特定于平台的确认对话框:

const string msg = "Are you sure you want to kill this process?";  
MessageBoxOptions options = new MessageBoxOptions(msg);  
options.Type = MessageBoxType.question;  
options.Buttons = new string[] {"No", "Yes"};  
options.DefaultId = 1;  
options.CancelId = 0;  
MessageBoxResult result = await Electron.Dialog.ShowMessageBoxAsync(options);

if (result.Response == 1)  
{  
    Process.Kill();  
    return RedirectToPage("Index");  
}

return Page();  

这样,如果用户取消,详细信息页面将保持当前状态。否则,应用程序在终止进程后重定向到主页。

Building Cross-Platform Desktop Apps with Electron.NET

自定义应用程序菜单

Electron.NET 提供了一个默认的应用程序菜单,如前所述。此菜单与 Electron 框架本身提供的默认菜单相同。不幸的是,无法调整默认菜单(Electron 的限制)。如果您想添加新命令或删除不需要的子菜单,您别无选择,只能从头开始指定整个菜单结构。由于 macOS 和其他平台之间的差异,此任务变得更加复杂。在 macOS 上,应用程序有自己的菜单,位于标准文件/编辑/视图菜单的左侧。

private void CreateMenu()  
{  
    bool isMac = RuntimeInformation.IsOSPlatform(OSPlatform.OSX);  
    MenuItem[] menu = null;

    MenuItem[] appMenu = new MenuItem[]  
    {  
        new MenuItem { Role = MenuRole.about },  
        new MenuItem { Type = MenuType.separator },  
        new MenuItem { Role = MenuRole.services },  
        new MenuItem { Type = MenuType.separator },  
        new MenuItem { Role = MenuRole.hide },  
        new MenuItem { Role = MenuRole.hideothers },  
        new MenuItem { Role = MenuRole.unhide },  
        new MenuItem { Type = MenuType.separator },  
        new MenuItem { Role = MenuRole.quit }  
    };

    MenuItem[] fileMenu = new MenuItem[]  
    {  
        new MenuItem { Role = isMac ? MenuRole.close : MenuRole.quit }  
    };

    MenuItem[] viewMenu = new MenuItem[]  
    {  
        new MenuItem { Role = MenuRole.reload },  
        new MenuItem { Role = MenuRole.forcereload },  
        new MenuItem { Role = MenuRole.toggledevtools },  
        new MenuItem { Type = MenuType.separator },  
        new MenuItem { Role = MenuRole.resetzoom },  
        new MenuItem { Role = MenuRole.zoomin },  
        new MenuItem { Role = MenuRole.zoomout },  
        new MenuItem { Type = MenuType.separator },  
        new MenuItem { Role = MenuRole.togglefullscreen }  
    };  

    if (isMac)  
    {  
        menu = new MenuItem[]  
        {  
            new MenuItem { Label = "Electron", Type = MenuType.submenu, Submenu = appMenu },  
            new MenuItem { Label = "File", Type = MenuType.submenu, Submenu = fileMenu },  
            new MenuItem { Label = "View", Type = MenuType.submenu, Submenu = viewMenu }  
        };  
    }  
    else  
    {  
        menu = new MenuItem[]  
        {  
            new MenuItem { Label = "File", Type = MenuType.submenu, Submenu = fileMenu },  
            new MenuItem { Label = "View", Type = MenuType.submenu, Submenu = viewMenu }  
        };  
    }

    Electron.Menu.SetApplicationMenu(menu);  
}

private async void CreateWindow()  
{  
    CreateMenu(); // add this line  
    var window = await Electron.WindowManager.CreateWindowAsync();  
    window.OnClosed += () => {  
        Electron.App.Quit();  
    };  
} 

请注意使用预定义菜单角色来指定目标平台的适当操作和快捷键。此外,由于 Electron.NET 序列化传递给 SetApplicationMenu 的参数的方式,您需要使用数组初始化器构建整个菜单结构。您不能将 MenuItem 实例附加到空数组,然后将其传递给菜单 API。

现在,当您在 Mac 上运行应用程序时,菜单栏将有三个名为 Electron、文件和视图的子菜单。

Electron.NET 还支持本机系统文件对话框。让我们修改“文件”菜单定义以添加“另存为”命令,该命令会提示用户输入文件名,并将当前进程列表以逗号分隔格式输出到该文件。

MenuItem[] fileMenu = new MenuItem[]  
{  
    new MenuItem { Label = "Save As...", Type = MenuType.normal, Click = async () => {  
        var mainWindow = Electron.WindowManager.BrowserWindows.First();  
        var options = new SaveDialogOptions() {  
            Filters = new FileFilter[] { new FileFilter{ Name = "CSV Files", Extensions = new string[] { "csv" } }  
        }};  
        string result = await Electron.Dialog.ShowSaveDialogAsync(mainWindow, options);  
        if (!string.IsNullOrEmpty(result))  
        {  
            string url = $"https://:{BridgeSettings.WebPort}/SaveAs?path={result}";  
            mainWindow.LoadURL(url);  
        }  
    }},  
    new MenuItem { Type = MenuType.separator },  
    new MenuItem { Role = isMac ? MenuRole.close : MenuRole.quit }  
};  

我们不指定预定义的菜单角色之一,而是将菜单类型设置为 normal 并提供一个异步 Click 处理程序。ShowSaveDialogAsync API 使用指定的选项打开一个本机文件对话框,在这种情况下,是 .csv 扩展名文件的过滤器。如果用户不取消对话框,API 将返回所选文件的完整路径。此路径用作 SaveAs Razor 页面的参数,该页面包含一个输出逗号分隔文本的 OnGetAsync 处理程序:

public async Task<IActionResult> OnGetAsync(string path)  
{  
    System.IO.StringWriter writer = new System.IO.StringWriter();  
    writer.WriteLine("Id,Process Name,Physical Memory");  

    var items = Process.GetProcesses().Where(p => !String.IsNullOrEmpty(p.ProcessName)).ToList();  
    items.ForEach(p => {  
        writer.Write(p.Id);  
        writer.Write(",");  
        writer.Write(p.MainModule.ModuleName);  
        writer.Write(",");  
        writer.WriteLine(p.WorkingSet64);  
     });

    await System.IO.File.WriteAllTextAsync(path, writer.ToString());  
    return RedirectToPage("Index");  
} 

Building Cross-Platform Desktop Apps with Electron.NET

添加第三方控件

与任何常规 ASP.NET Core 网站一样,您可以将第三方控件添加到 Electron.NET 应用程序。让我们用 ComponentOne FlexChart 控件替换默认的隐私页面,该控件按物理内存使用量的降序绘制前十个进程。

首先,将以下包引用添加到 .csproj 文件中:

<PackageReference Include="C1.AspNetCore.Mvc" Version="3.0.20202.*" />  

编辑 Pages/_ViewImports.cshtml 并附加相应的标签助手:

@addTagHelper *, C1.AspNetCore.Mvc  

编辑 Pages/Shared/_Layout.cshtml 并在结束 </head> 标签之前插入以下行:

<c1-styles />  
<c1-scripts>  
    <c1-basic-scripts />  
</c1-scripts>  

然后,在同一个文件中,将前两个 Privacy 替换为 Chart

编辑 Startup.cs 并将 UseEndpoints 调用替换为以下内容:

app.UseEndpoints(endpoints =>  
{  
    endpoints.MapControllerRoute(  
        name: "default",  
        pattern: "{controller=Home}/{action=Index}/{id?}");

    endpoints.MapRazorPages();  
});  

需要调用 MapControllerRoute 才能使 MVC 控件正确加载其资源。

最后,在 Pages 文件夹中创建一个名为 Chart.cshtml 的文件,并添加以下标记:

@page  
@model ChartModel  
@{  
    ViewData["Title"] = "Chart view";  
}  
<div>  
<c1-flex-chart binding-x="Name" chart-type="Bar" legend-position="None">  
    <c1-items-source source-collection="@Model.Processes"></c1-items-source>  
    <c1-flex-chart-series binding="Memory" name="Physical Memory" />  
    <c1-flex-chart-axis c1-property="AxisX" position="None" />  
    <c1-flex-chart-axis c1-property="AxisY" reversed="true" />  
</c1-flex-chart>  
</div>  

然后添加相应的代码隐藏文件 Chart.cshtml.cs

using System;  
using System.Collections.Generic;  
using System.Linq;  
using System.Threading.Tasks;  
using Microsoft.AspNetCore.Mvc;  
using Microsoft.AspNetCore.Mvc.RazorPages;  
using Microsoft.Extensions.Logging;  
using System.Diagnostics;

namespace Processes.Pages  
{  
    public class ChartModel : PageModel  
    {  
        public List<object> Processes { get; set; }  
        private readonly ILogger<ChartModel> _logger;

        public ChartModel(ILogger<ChartModel> logger)  
        {  
            _logger = logger;  
        }

        public void OnGet()  
        {  
            var items = Process.GetProcesses()  
                .Where(p => !String.IsNullOrEmpty(p.ProcessName))  
                .OrderByDescending(p => p.WorkingSet64)  
                .Take(10);

            Processes = items.Select(p => new {  
                Id = p.Id,  
                Name = p.ProcessName,  
                Memory = p.WorkingSet64  
            }).ToList<object>();  
        }  
    }  
}

保存所有更改。请注意,构建过程将为 ComponentOne Studio Enterprise 创建 30 天试用许可证。单击菜单栏中的“图表”链接,您应该会看到一个类似于以下的条形图:

Building Cross-Platform Desktop Apps with Electron.NET

为其他平台构建

要为其他平台构建安装介质,请在终端窗口中运行以下命令:

electronize build /target xxx /PublishReadyToRun false  

其中 xxxwinlinuxosx 之一。输出到 bin/Desktop 文件夹,例如:

  • Processes Setup 1.0.0.exe (Windows)
  • Processes-1.0.0.AppImage (Linux)
  • Processes-1.0.0.dmg (OSX)

请注意,Windows 可执行文件是一个安装程序,而不是应用程序本身。此外,OSX 目标只能在 Mac 上构建,但 Windows/Linux 目标可以在任何平台上构建。要更改版本号、版权声明和其他属性,请在创建安装介质之前编辑 electron.manifest.json

这是应用程序在 Linux 上运行时的样子:

Building Cross-Platform Desktop Apps with Electron.NET

结论和示例代码

Electron.NET 是一个开源工具,通过为 C# 开发人员提供为 Windows、Linux 和 macOS 交付跨平台桌面应用程序的途径,为 ASP.NET Core 增值。它还与 ComponentOne Studio Enterprise 中的 MVC 控件等第三方组件兼容。

本文描述的已完成项目的源代码可在 GitHub 上获取。

© . All rights reserved.