本文内容源自《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>
待续。。。