基於 WPF 模塊化架構下的本地化設計實踐

来源:https://www.cnblogs.com/hippieZhou/archive/2019/08/13/11335110.html
-Advertisement-
Play Games

背景描述 最近接到一個需求,就是要求我們的 WPF 客戶端具備本地化功能,實現中英文多語言界面。剛開始接到這個需求,其實我內心是拒絕的的,但是沒辦法,需求是永無止境的。所以只能想辦法解決這個問題。 首先有必要說一下我們的系統架構。我們的系統是基於 Prism 來進行設計的,所以每個業務模塊之間都是相 ...


背景描述

最近接到一個需求,就是要求我們的 WPF 客戶端具備本地化功能,實現中英文多語言界面。剛開始接到這個需求,其實我內心是拒絕的的,但是沒辦法,需求是永無止境的。所以只能想辦法解決這個問題。

首先有必要說一下我們的系統架構。我們的系統是基於 Prism 來進行設計的,所以每個業務模塊之間都是相互獨立,互不影響的 DLL,然後通過主 Shell 來進行目錄的動態掃描來實現動態載入。

為了保證在不影響系統現有功能穩定性的前提下,如何讓所有模塊支持多語言成為了一個亟待解決的問題。

剛開始,我 Google 了一下,查閱了一些資料,很多都是介紹如何在單體程式中實現多語言,但是在模塊化架構中,我個人覺得這樣做並不合適。做過本地化的朋友應該都知道,在進行本地化翻譯的時候,都需要創建對應語言的資源文件,無論是使用 .xaml .resx.xml,這裡面會存放我們的本地化資源。對於單體系統而言,這些資源直接放到主程式下即可,方便快捷。但是對於模塊化架構的程式,這樣做就不太好,而是應該將這些資源都分別放到自己模塊內部由自己來維護,主程式只需規定整個系統的區域語言即可。

設計思路

面對上面的背景描述,我們可以大致描述一下我們期望的解決方式,主程式只負責對整個系統進行區域語言設置,每個模塊的本地化由本模塊內部完成,所有模塊的本地化切換方式保持一致,依賴於共有的一種實現。如下圖所示:

實現方案

由於如何使用 Prism 不是本文的重點,所以這裡就略過主程式和模塊程式中相關的模板代碼,感興趣的小伙伴可以自行在園子里搜索相關技術文章。

參照上述的思路,我們可以做一個小示例來展示一下如何進行多模塊多語言的本地化實踐。

在這個示例中,我以 DotNetCore 3.0 版本的 WPF 和 Prism 進行示例說明。在我們的示例工程中創建三個項目

  • BlackApp
    • 引用 Prism.Unity 包
    • WPF App(.NET Core 版本),作為啟動程式
  • BlackApp.ModuleA
    • 引用 Prism.Wpf 包
    • WPF UseControl(.NET Core 版本),作為示例模塊
  • BlackApp.Common
    • ClassLibrary(.NET Core 版本),作為基礎的公共服務層

BlackApp.ModuleA 添加對 BlackApp.Common 的引用,並將 BlackApp 和 BlackApp.ModuleA 的項目輸出修改為相同的輸出目錄。然後修改對應的基礎代碼,以確保主程式能正常載入並顯示 ModuleA 模塊及其內容。

上述操作完成後,我們就可以編寫我們的測試代碼了。按照我們的設計思路,我需要先在 BlackApp.ModuleA 定義我們的本地化資源文件,對於這個資源文件的類型選擇,理論上我們是可以選擇任何一種基於 XML 的文件,但是不同類型的文件對於後面是否是埋坑行為這個需要認真考慮一下。這裡我建議使用 XAML 格式的文件。我們在 BlackApp.ModuleA 項目的根目錄下創建一個 Strings 的文件夾,然後裡面分別創建 en-US.xamlzh-CN.xaml 文件。這裡建議最好以語言名稱作為文件名稱,這樣方便到時候查找。文件內容如下所示:

  • en-US.xaml
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:BlackApp.ModuleA.Strings"
    xmlns:system="clr-namespace:System;assembly=System.Runtime">
    <system:String x:Key="string1">Hello world</system:String>
</ResourceDictionary>
  • zh-CN.xaml
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:BlackApp.ModuleA.Strings"
    xmlns:system="clr-namespace:System;assembly=System.Runtime">
    <system:String x:Key="string1">世界你好</system:String>
</ResourceDictionary>

資源文件定義好了,接下來就是如何使用了。

對於我們需要進行本地化的 XAML 頁面,首先我們需要指當前使用到的資源文件,這個時候就需要在我們的 BlackApp.Common 項目中定義一個依賴屬性了,然後通過依賴屬性的方式來進行設置。由於語言種類有很多,所以我們定義一個文件夾目錄的依賴屬性,來指定當前頁面需要用到的資源的文件夾路徑,然後由輔助類到時候依據具體的語言類型來到指定目錄查找指當的資源文件。
示例代碼如下所示:

[RuntimeNameProperty(nameof(ExTranslationManager))]
public class ExTranslationManager : DependencyObject
{
    public static string GetResourceDictionary(DependencyObject obj)
    {
        return (string)obj.GetValue(ResourceDictionaryProperty);
    }

    public static void SetResourceDictionary(DependencyObject obj, string value)
    {
        obj.SetValue(ResourceDictionaryProperty, value);
    }

    // Using a DependencyProperty as the backing store for ResourceDictionary.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ResourceDictionaryProperty =
        DependencyProperty.RegisterAttached("ResourceDictionary", typeof(string), typeof(ExTranslationManager), new PropertyMetadata(null));

}

本地化資源指定完畢後,我們就可以使用裡面資源文件進行本地化操作。如果想在 XAML 對相應屬性進行 標簽式 訪問,需要定義一個繼承自 MarkupExtension 類的自定義類,併在該類中實現 ProvideValue 方法。接下來在我們的 BlackApp.Common 項目中定義該類,示例代碼如下所示:

[RuntimeNameProperty(nameof(ExTranslation))]
public class ExTranslation : MarkupExtension
{
    public string StringName { get; private set; }
    public ExTranslation(string stringName)
    {
        this.StringName = stringName;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        object targetObject = (serviceProvider as IProvideValueTarget)?.TargetObject;

        ResourceDictionary dictionary = GetResourceDictionary(targetObject);
        if (dictionary == null)
        {
            object rootObject = (serviceProvider as IRootObjectProvider)?.RootObject;
            dictionary = GetResourceDictionary(rootObject);
        }

        if (dictionary == null)
        {
            if (targetObject is FrameworkElement frameworkElement)
            {
                dictionary = GetResourceDictionary(frameworkElement.TemplatedParent);
            }
        }

        return dictionary != null && StringName != null && dictionary.Contains(StringName) ?
            dictionary[StringName] : StringName;
    }

    private ResourceDictionary GetResourceDictionary(object target)
    {
        if (target is DependencyObject dependencyObject)
        {
            object localValue = dependencyObject.ReadLocalValue(ExTranslationManager.ResourceDictionaryProperty);
            if (localValue != DependencyProperty.UnsetValue)
            {
                var local = localValue.ToString();
                var (baseName,stringName) = SplitName(local);
                var str = $"pack://application:,,,/{baseName};component/{stringName}/{Thread.CurrentThread.CurrentCulture}.xaml";
                var dict = new ResourceDictionary { Source = new Uri(str) };
                return dict;
            }
        }
        return null;
    }

    public static (string baseName, string stringName) SplitName(string name)
    {
        int idx = name.LastIndexOf('.');
        return (name.Substring(0, idx), name.Substring(idx + 1));
    }
}

此外,如果我們的 ViewModel 中也有數據需要進行本地化操作的化,我們可以定義一個擴展方法,示例代碼如下所示:

public static class ExTranslationString
{
    public static string GetTranslationString(this string key, string resourceDictionary)
    {
        var (baseName, stringName) = ExTranslation.SplitName(resourceDictionary);
        var str = $"pack://application:,,,/{baseName};component/{stringName}/{Thread.CurrentThread.CurrentCulture}.xaml";
        var dictionary = new ResourceDictionary { Source = new Uri(str) };
        return dictionary != null && !string.IsNullOrWhiteSpace(key) && dictionary.Contains(key) ? (string)dictionary[key] : key;
    }
}

通過在 BlackApp.Common 中定義上述 3 個輔助類,基本可以滿足我們的需求,我們可以卻換到 BlackApp.ModuleA 項目中,併進行如下示例修改

  • View 層使用示例
<UserControl
    x:Class="BlackApp.ModuleA.Views.MainView"
    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:ex="clr-namespace:BlackApp.Common;assembly=BlackApp.Common"
    xmlns:local="clr-namespace:BlackApp.ModuleA.Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:prism="http://prismlibrary.com/"
    d:DesignHeight="300"
    d:DesignWidth="300"
    ex:ExTranslationManager.ResourceDictionary="BlackApp.ModuleA.Strings"
    prism:ViewModelLocator.AutoWireViewModel="True"
    mc:Ignorable="d">
    <Grid>
        <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
            <TextBlock Text="{Binding Message}" />
            <TextBlock Text="{ex:ExTranslation string1}" />
        </StackPanel>
    </Grid>
</UserControl>
  • ViewModel 層使用示例

"message".GetTranslationString("BlackApp.ModuleA.Strings")

最後,我們就可以在我們的 BlackApp 項目中的 App.cs 構造函數中來設置我們程式的語言類型,示例代碼如下所示:

public partial class App
{
    public App()
    {
        //CultureInfo ci = new CultureInfo("zh-cn");
        CultureInfo ci = new CultureInfo("en-US");
        Thread.CurrentThread.CurrentCulture = ci;
    }
    protected override Window CreateShell()
    {
        return Container.Resolve<MainWindow>();
    }

    protected override void RegisterTypes(IContainerRegistry containerRegistry)
    {

    }

    protected override IModuleCatalog CreateModuleCatalog()
    {
        return new DirectoryModuleCatalog() { ModulePath = AppDomain.CurrentDomain.BaseDirectory };
    }
}

寫到這裡,我們應該就可以進行本地化的測試工作了,嘗試編譯運行我們的示常式序,如果不出意外的話,應該是可以通過在 主程式中設置區域類型來更改模塊程式中的對應本地化資源內容。

最後,整個示例項目的組織結構如下圖所示:

總結

對於模塊化架構的本地化實現,有很多的實現方式,我這裡介紹的只是一種符合我們的業務場景的一種實現,期待大佬們在評論區留言提供更好的解決方案。

補充

經同事驗證,使用 .resx 格式的資源文件會更簡單一下,可以直接通過

 BlackApp.ModuleA.Strings.zh_cn.ResourceManager("string1")
 BlackApp.ModuleA.Strings.en_us.ResourceManager("string1")

的方式來訪問。但前提是需要將對應資源文件的訪問修飾符設置為 public

參考


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1、在電腦科學中,二叉樹是每個結點最多有兩個子樹的樹結構。通常子樹被稱作“左子樹”(left subtree)和“右子樹”(right subtree)。(百度百科) 廣度優先搜索(Breadth First Search),又叫寬度優先搜索或橫向優先搜索,是從根結點開始沿著樹的寬度搜索遍歷,上面 ...
  • 一、Web Service 1、定義 是可以接收從Internet上的其他系統中傳遞的請求,是一種輕量級的獨立的通訊技術, 能使得運行在不同機器上的不同應用無須藉助附加的、專門的第三方軟體或硬體, 就可相互交換數據或集成。所以它是一個平臺獨立,低耦合,自包含,基於可編程的Web應用程式,適用於開發分 ...
  • 後端處理:var callback=context.Request.QueryString["callback"].ToString(); context.Response.Write($"{callback}({SerializeObject(result)})"); result為返回的數據 前 ...
  • (雙擊全屏播放) 一、前言 為什麼選擇Hyper-V? windowns自帶,免費 基礎環境 二、虛擬機配置 下載CentOS7鏡像 https://www.centos.org/download/ 此次安裝使用的版本為:CentOS-7-x86_64-Minimal-1611.iso 打開Hype ...
  • PostgreSQL是一個功能強大的開源資料庫系統。它支持了大多數的SQL:2008標準的數據類型,包括整型、數值值、布爾型、位元組型、字元型、日期型、時間間隔型和時間型,它也支持存儲二進位的大對像,包括圖片、聲音和視頻。PostgreSQL對很多高級開發語言有原生的編程介面,如C/C++、Java、... ...
  • 大家好,前幾天因工作需要要開發一個基於WinForm的小程式。其中要用到分頁,最開始的想法找個第三方的dll用一下,但是後來想了想覺得不如自己寫一個玩一下 之前的web開發中有各式各樣的列表組件基本都帶有分頁功能,筆者早先也自己寫過B/S端的分頁組件(利用jquery純前端方式)。對於WinForm ...
  • Virtual方法(虛方法) Virtual方法(虛方法) virtual 關鍵字用於在基類中修飾方法。virtual的使用會有兩種情況: 情況1:在基類中定義了virtual方法,但在派生類中沒有重寫該虛方法。那麼在對派生類實例的調用中,該虛方法使用的是基類定義的方法。 情況2:在基類中定義了vi ...
  • 2019年9月23——25日 .NET Core 3.0即將在.NET Conf上發佈! .NET Core的發佈及成熟重燃了.net程式員的熱情和希望,一些.net大咖也在積極的為推動.NET Core而不懈的努力。在這次.NET Core 3.0中一項新的技術也首次出現在人們的視野,這就是Bla ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...