WinUI学习之Cultures in Action项目

2021-11-24

winui zinotes

本文内容源自《PROFESSIONAL C# and .NET 2021 Edition》这本书的CHAPTER 22 Localization中的示例项目Cultures in Action。这是一本很新的书,写的内容也是很新,Windows App的技术使用的是WinUI,Cultures in Action示例项目涉及的内容包括:

  • Treeview

界面的左侧是一个Treeview,用来显示Culture列表,右侧是一个自定义的空间, 显示左侧所选Culture的相关信息。应用初始化时会将所有的Culture加到左侧的Treeview控件,这个操作是在CulturesViewModel类的构造函数里实现的,构造函数会调用Model类的SetupCultures方法来做初始化。代码位于WinUICultureDemo/CulturesViewModel.cs:

<pre class="EnlighterJSRAW" data-enlighter-group="" data-enlighter-highlight="" data-enlighter-language="csharp" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-theme="" data-enlighter-title="">public CulturesViewModel() => SetupCultures();

自定义的类CultureData用于处理Treeview中的数据,这个类可以绑定到Treeview,因为它有一个SubCultures属性,包含了一个CultureData列表,这样Treeview就能够遍历整个列表。除了SubCultures属性,CultureData还包含CultureInfo类型,和一些示例数据字段(包括NumberSample、DateSample、TimeSample)。NumberSample、DateSample分别是对应Culture的数字和日期的格式字符串。CultureData还有一个RegionInfo类型的属性保存region信息。对于一些中性化的Culture(例如English),创建RegionInfo会抛出异常,因为有的region只属于特定的Culture。而对于其他的Culture(例如German),则会成功创建RegionInfo并指向默认的region。这里抛出的异常会被处理,参考代码WinUICultureDemo/CultureData.cs:

<pre class="EnlighterJSRAW" data-enlighter-group="" data-enlighter-highlight="" data-enlighter-language="csharp" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-theme="" data-enlighter-title="">public record CultureData(CultureInfo CultureInfo)
{
    public IList<CultureData> SubCultures { get; } = new List<CultureData>();
    double numberSample = 9876543.21;
    public string NumberSample => numberSample.ToString("N", CultureInfo);
    public string DateSample => DateTime.Today.ToString("D", CultureInfo);
    public string TimeSample => DateTime.Now.ToString("T", CultureInfo);
    private RegionInfo? _regionInfo;
    public RegionInfo? RegionInfo
    {
       get
        {
           try
        {
           return _regionInfo ??= new RegionInfo(CultureInfo.Name);
        }
        catch (ArgumentException)
        {
           // with some neutral cultures regions are not available
           return null;
        }
           return ri;
        }
    }
}

在SetupCultures方法中,通过精通方法CultureInfo.GetCultures获取全部的Culture。通过传参数CultureTypes.AllCultures,能拿到未排序的全量数据,返回结果按名字排序。foreach语句将创建用于在界面上展示的CultureData对象列表,这个列表包含了Treeview展示的所有根Culture数据。如果LCID的值是0x7f,那么这个CultureInfo是根数据,否则需要加到正确的父节点下面。代码如下(WinUICultureDemo/CulturesViewModel.cs):

<pre class="EnlighterJSRAW" data-enlighter-group="" data-enlighter-highlight="" data-enlighter-language="csharp" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-theme="" data-enlighter-title="">private void SetupCultures()
{
    var cultureDataDict = CultureInfo.GetCultures(CultureTypes.AllCultures)
    .OrderBy(c => c.Name)
    .Select(c => new CultureData(c))
    .ToDictionary(c => c.CultureInfo.Name);
    List<CultureData> rootCultures = new();
    foreach (var cd in cultureDataDict.Values)
    {
        if (cd.CultureInfo.Parent.LCID == 0x7f) // check for invariant culture
        {
            rootCultures.Add(cd);
        }
        else // add to parent culture
        {
            if (cultureDataDict.TryGetValue(cd.CultureInfo.Parent.Name,
            out CultureData? parentCultureData))
            {
                parentCultureData.SubCultures.Add(cd);
                continue;
            }
            // with the latest culture updates, some cultures don't have the 
            // direct parent name in the list, take the next parent
            string parent = cd.CultureInfo.Parent.Name;
            int index = parent.IndexOf("-");
            if (index < 0)
            {
                // just add this culture to the root cultures
                rootCultures.Add(cd);
                continue;
            }
            string grandParent = parent[..index];
            if (cultureDataDict.TryGetValue(grandParent,
            out CultureData? grandParentCultureData))
            {
                grandParentCultureData.SubCultures.Add(cd);
            }
            else // parent also not found to the root cultures, add it directly
            {
                rootCultures.Add(cd);
            }
        }
    }
    foreach (var rootCulture in rootCultures.OrderBy(cd => cd.CultureInfo.EnglishName))
    {
        RootCultures.Add(rootCulture);
    }
}

public IList<CultureData> RootCultures { get; } = new List<CultureData>();

再来看看XAML文件,Treeview控件用来展示全部的Culture,Treeview中每一项数据的展示由一个模板控制,模板中有一个TextBlock字段,与CultureInfo的EnglishName属性绑定,参考代码(WinUICultureDemo/MainWindow.xaml):

<pre class="EnlighterJSRAW" data-enlighter-group="" data-enlighter-highlight="" data-enlighter-language="csharp" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-theme="" data-enlighter-title=""><TreeView x:Name="treeView1" 
     Style="{StaticResource TreeViewStyle1}"
     ItemInvoked="{x:Bind OnSelectionChanged, Mode=OneTime}"
     SelectionMode="Single">
</TreeView>

来看看背后的代码,TreeView初始化时会通过viewmodel访问CultureData对象,通过CultureData对象数据来创建TreeView的TreeNode节点,TreeNode类定义了一个Data属性,CultureData被赋值到这个Data属性上,然后通过AddSubNodes方法递归添加子对象。代码位于WinUICultureDemo/MainWindow.xaml.cs:

<pre class="EnlighterJSRAW" data-enlighter-group="" data-enlighter-highlight="" data-enlighter-language="csharp" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-theme="" data-enlighter-title="">private void OnActivated(object sender, WindowActivatedEventArgs args)
{
    void AddSubNodes(TreeViewNode parent)
    {
        if (parent.Content is CultureData cd && cd.SubCultures is not null)
        {
            foreach (var culture in cd.SubCultures)
            {
                TreeViewNode node = new()
                {
                    Content = culture
                };
                parent.Children.Add(node);
                foreach (var subCulture in culture.SubCultures)
                {
                    AddSubNodes(node);
                }
            }
        }
    }
    var rootNodes = ViewModel.RootCultures.Select(cd => new TreeViewNode
    {
        Content = cd
    });
    foreach (var node in rootNodes)
    {
        treeView1.RootNodes.Add(node);
        AddSubNodes(node);
    }
}

用户选择TreeView中的一个节点时,SelectedItemChanged事件被触发,如下面的代码片段所示,OnSelectionChanged方法实现了这个事件的逻辑。可以看到,TreeView绑定的ViewModel的SelectedCulture属性被指定到所选的CultureData对象。代码位于WinUICultureDemo/MainWindow.xaml.cs:

<pre class="EnlighterJSRAW" data-enlighter-group="" data-enlighter-highlight="" data-enlighter-language="csharp" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-theme="" data-enlighter-title="">private void OnSelectionChanged(TreeView sender, TreeViewItemInvokedEventArgs args)
{
    if (args.InvokedItem is TreeViewNode node && node.Content is CultureData cd)
    {
        ViewModel.SelectedCulture = cd;
    }
}

展示节点用到了若干TextBlock控件,这些控件与CultureData类的CultureInfo属性绑定,进而与返回的CultureInfo的属性绑定,这些属性包括Name, IsNeutralCulture, EnglishName, NativeName等等。我们可以通过转换器来处理一些属性的展示内容,例如把Bool类型(IsNeutralCulture属性)转换成友好的枚举值,或者显示日期内容等等,对应的XAML文件位于WinUICultureDemo/CultureDetailUC.xaml:

<pre class="EnlighterJSRAW" data-enlighter-group="" data-enlighter-highlight="" data-enlighter-language="csharp" data-enlighter-linenumbers="" data-enlighter-lineoffset="" data-enlighter-theme="" data-enlighter-title=""><TextBlock Grid.Row="0" Grid.Column="0" Text="Culture Name:"/>
<TextBlock Grid.Row="0" Grid.Column="1"  Text="{x:Bind CultureData.CultureInfo.Name, Mode=OneWay}"
 Width="100"/>
<TextBlock Grid.Row="0" Grid.Column="2" Text="Neutral Culture" Visibility="{x:Bind CultureData.CultureInfo.IsNeutralCulture, Mode=OneWay}"/>
<TextBlock Grid.Row="1" Grid.Column="0" Text="English Name:"/>
<TextBlock Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2" Text="{x:Bind CultureData.CultureInfo.EnglishName, Mode=OneWay}"/>
<TextBlock Grid.Row="2" Grid.Column="0" Text="Native Name:"/>
<TextBlock Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" Text="{x:Bind CultureData.CultureInfo.NativeName}"/>
<TextBlock Grid.Row="3" Grid.Column="0" Text="Default Calendar:"/>
<TextBlock Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="2" Text="{x:Bind CultureData.CultureInfo.Calendar, Mode=OneWay Converter={StaticResource calendarConverter}}"/>
<TextBlock Grid.Row="4" Grid.Column="0" Text="Optional Calendars:"/>
<ListBox Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2" ItemsSource="{x:Bind CultureData.CultureInfo.OptionalCalendars}">
        <ListBox.ItemTemplate>
        <DataTemplate>
        <TextBlock Text="{Binding Converter={StaticResource calendarConverter}}"/>
    </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

待续。。。

 
阅读