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

使用 Codon 为 UWP 创建设置屏幕

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2019年3月29日

CPOL

8分钟阅读

viewsIcon

8308

downloadIcon

93

为您的 UWP 应用创建一个动态的设置屏幕。只需一行代码即可添加一项设置。

目录

引言

几乎所有应用程序都需要一个设置屏幕。许多开发人员选择简单地构建静态 UI;将按钮和文本字段硬编码到设置后端存储中。但是,如果这样做,随着设置数量的增加,技术债务就会增加;使得将设置屏幕重构为类别,或更改设置的存储或显示方式,变得越来越困难。

这就是为什么我在 CodonFX 中构建了一个选项系统,它与设置系统和隔离存储后端存储集成,可以替换为 SQLite 后端存储。CodonFX 中的选项系统可能为我节省了数月的开发时间,并允许我轻松地完成一些非常好的事情,例如导出和导入选项。

添加选项

借助 Codon 选项系统,只需一行代码即可在选项屏幕上实现一个选项,该选项会自动将其写入后端存储。请参见以下内容

generalOptions.Add(new BooleanUserOption(() => "Boolean 1 title", "Boolean1Key", () => false));

在这里,我们创建了一个 BooleanUserOption,它在选项屏幕上呈现为 ToggleSwitch,并使用指定的字符串键自动将其值写入 Codon 的 ISettingsService。选项的标题是一个 lambda 表达式,允许您轻松地对其进行本地化,因此如果 UI 语言发生变化,标题将以正确的语言显示。

有许多内置的选项类型,代表常见的设置类型,包括

  • BooleanUserOption
  • DoubleUserOption
  • IntUserOption
  • ObjectUserOption
  • StringUserOption

还有其他一些用于以不同方式呈现设置的选项,包括

  • CommandOption
  • CompositeOption
  • ListOption
  • RangeUserOption

CommandOption 用于显示一个按钮,该按钮在单击/点击时执行 ICommand

CompositeOption 允许您创建自己的自定义行为,其中可能包含在自定义 UI 中呈现的多个选项。

ListOption 允许您例如显示一个值枚举,该枚举呈现为下拉列表。

RangeUserOption 可用于向用户显示滑块,用于将 double 或 int 值写入后端存储。

当然,您可以创建自定义的 IUserOption 类,以满足您应用程序的需求。

示例概述

我为 UWP 准备了一个 小型示例 来在 UWP 应用中演示它。Codon 是跨平台的,您可以在 Surfy Browser for Android 等应用中看到选项系统的实际运行情况。

在示例中,您可以看到项目:一个 UWP 应用项目和一个 .NET Standard 类库。用户选项系统位于 Codon.Extras.Core NuGet 包中,该包由类库引用。UWP 应用项目引用了 Codon.Extras.Uwp NuGet 包。

探索 .NET Standard 库

类库包含各种类,包括 Bootstrapper 类,当应用程序启动时会调用其 Run 方法。

虽然不是绝对必要,但 AppSettings 类具有表示设置的强类型属性,这使您在编译时就能确信您的设置被正确引用。

位于 AppSettings.UserOptions.cs 的部分类负责注册将在 OptionsPage 上显示的选项。总的来说,用户选项代表了设置的一个子集。

AppSettings 类的 ConfigureUserOptions 方法需要 IUserOptionsService。请参见清单 1。

userRoles 参数仅用于演示如何根据用户的权限显示不同的一组选项。这更适用于企业场景。我在我的一个应用程序中这样做,您可能不需要它。

IUserOptionsService 实现允许通过其 Register 方法注册多个选项类别。它接受一个 OptionCategory 方法和一列应显示在该类别中的选项。在我的一些应用程序中,我以选项卡或有时是可展开组的形式显示选项类别。

当您将选项添加到类别的选项集合时,它会自动显示在选项页面上。每个 IUserOption 对象都必须有一个唯一的键。lambda 表达式用于大多数属性,以允许您的应用程序在不重启的情况下切换语言。

您可以通过设置选项的 TemplateFunc 属性来指定选项的模板。

有关更多信息,请参阅 UserOptionBase 实现。

清单 1. AppSettings.ConfigureUserOptions 方法。

partial class AppSettings
{
	public void ConfigureUserOptions(IUserOptionsService userOptionsService, UserRoles userRoles)
	{
		OptionCategory defaultCategory = new OptionCategory(OptionCategoryIds.General, () => "General");

		var generalOptions = new List<IUserOption>
		{
			new StringUserOption(() => "String 1", String1Key, () => string1DefaultValue),
			new BooleanUserOption(() => "Boolean 1", Boolean1Key, () => boolean1DefaultValue)
		};
		
		userOptionsService.Register(generalOptions, defaultCategory);
	}
}

我提到 AppSettings 类并非真正必需。但我喜欢使用它,这样我就可以轻松地重构设置名称,而不用担心破坏某些东西。

为完整起见,我想提一下,我使用 ReSharper 的实时模板来定义一个设置,如清单 2 所示。

$SettingName$$Type$ 是唯二可编辑的值。$SettingName$ 在模板的属性网格中定义为“为变量建议名称”。$Type$ 设置为“猜测此处预期的类型”。

根据您定义 AppSettings 类的位置,您可能希望将设置名称和 setter 的可见性从 private 更改为 internalpublic

清单 2. ReSharper 实时模板用于设置

public const string $SettingName$Key = "$SettingName$";
static $Type$ $SettingNameLower$DefaultValue = $DefaultValue$;

public $Type$ $SettingName$
{
	get => settingsService.GetSetting($SettingName$Key, $SettingNameLower$DefaultValue);
	private set => settingsService.SetSetting($SettingName$Key, value);
}

当应用程序启动时,Bootstrapper 类的 Run 方法通过 UWP 应用项目中的 App 类被调用,如以下摘录所示

protected override void OnLaunched(LaunchActivatedEventArgs e)
{
	...
	if (!bootstrapperRan)
	{
		bootstrapperRan = true;
		var bootstrapper = new Bootstrapper();
		bootstrapper.Run();
	}
	...
}

有时,您的引导程序可能需要执行一些异步活动,在这种情况下,您需要将 Run 方法设为 async,并妥善处理错误。

Bootstrapper 类将 AppSettings 类注册为单例。请参见清单 3。

提示: 根据您的应用程序需求,您可能需要为每个平台实现一个特定于平台的引导程序。我通常会这样做,并让特定于平台的引导程序调用非特定于平台的引导程序的 Run 方法。

您可能会注意到代码中 AppSettings 类需要一个 ISettingsService 实例作为构造函数参数。依赖注入用于解析默认实例,使用 Codon 框架的默认 IoC 容器 FrameworkContainer 类。

如果您对 Codon 框架的内部机制感兴趣,FrameworkContainer 使用接口属性来查找默认类型映射,使用 Codon 的 DefaultTypeDefaultTypeName 属性;或者 .NET Standard 的 DefaultValueAttribute。如果您查看 ISettingsService 源代码,您可以看到它是如何用 DefaultTypeDefaultTypeName 属性进行装饰的。DefaultTypeName 具有优先权,用于查找接口的特定于平台的实现(如果存在)。如果找不到由 DefaultTypeName 标识的类型,容器将回退到 DefaultType

我们可以在实例化 AppSettings 类时配置用户选项,这样我们就可以通过 DI 传递 IUserOptionsService,但我选择使用显式方法调用,因为我们可能希望推迟配置用户选项以优化应用程序启动时间。

清单 3. Bootstrapper 类。

public class Bootstrapper
{
	public void Run()
	{
		Dependency.Register<AppSettings, AppSettings>(true);

		var appSettings = Dependency.Resolve<AppSettings>();
		appSettings.ConfigureUserOptions(Dependency.Resolve<IUserOptionsService>(), UserRoles.User);
	}
}

在 UWP 中渲染选项

类库项目中的 OptionsViewModel 类包含一个 Groupings 属性,该属性从 IUserOptionsService 中检索 UserOptionGroupings,如下所示

public IUserOptionGroupings Groupings => 
			Dependency.Resolve<IUserOptionsService>().UserOptionGroupings;

OptionsPage 类通过 IoC 容器公开 OptionsViewModel 的实例,如清单 4 所示。

我们在 XAML 页面上同时使用 x:Bindx:Binding 表达式,因此 OptionsViewModel 实例既作为属性公开,又设置为页面的 DataContext。如果您不知道,x:Bind 的上下文是 Page 的一个属性,而传统的 x:Binding 表达式在使用属性时使用 Page 的 DataContext

清单 4. OptionsPage 类

public sealed partial class OptionsPage : Page
{
	public OptionsPage()
	{
		this.InitializeComponent();
		DataContext = Dependency.Resolve<OptionsViewModel, OptionsViewModel>(true);
	}

	public OptionsViewModel ViewModel
	{
		get => DataContext as OptionsViewModel;
		set => DataContext = value;
	}
}

在 OptionsPage.xaml 文件中,您会看到页面的资源包括一个 CollectionViewSource 声明,其 Source 属性绑定到视图模型的 Groupings 属性。请参见清单 5。

我们使用自定义模板选择器来确定用于 CollectionViewSource 中每个选项的 DataTemplate

清单 5. OptionsPage Resources 元素

<Page.Resources>
	<CollectionViewSource x:Key="optionsViewSource"  
		IsSourceGrouped="True" Source="{x:Bind ViewModel.Groupings}" />

	<local:OptionTemplateSelector 
		x:Key="optionTemplateSelector" 
		Templates="{StaticResource OptionTemplateCollection}">
	</local:OptionTemplateSelector>
</Page.Resources>

OptionTemplateSelector 具有一个 Templates 属性,该属性绑定到 App.xaml 中找到的资源。请参见清单 6。

NamedTemplateCollection 包含用于显示每个用户选项的所有模板。有一个String 模板和一个Boolean 模板。StringBoolean 的名称分别映射到 StringUserOptionBooleanUserOption 类的 TemplateName 属性。

注意:您可以通过设置用户选项的 TemplateName 属性来覆盖它使用的模板。

清单 6. NamedTemplateCollection 元素

<local:NamedTemplateCollection x:Key="OptionTemplateCollection">
	<local:NamedTemplate Name="String">
		<local:NamedTemplate.DataTemplate>
			<DataTemplate>
				<TextBox 
					Header="{Binding UserOption.Title, Mode=OneWay}" 
					Text="{Binding Setting, Mode=TwoWay}" 
					Style="{StaticResource OptionBox}" />
			</DataTemplate>
		</local:NamedTemplate.DataTemplate>
	</local:NamedTemplate>
	<local:NamedTemplate Name="Boolean">
		<local:NamedTemplate.DataTemplate>
			<DataTemplate>
				<ToggleSwitch 
					Header="{Binding UserOption.Title, Mode=OneWay}" 
					IsOn="{Binding Setting, Mode=TwoWay}" 
					Margin="{StaticResource OptionItemMargin}" />
			</DataTemplate>
		</local:NamedTemplate.DataTemplate>
	</local:NamedTemplate>
</local:NamedTemplateCollection>

自定义模板选择器名为 OptionTemplateSelector,它继承自 Windows.UI.Xaml.Controls.DataTemplateSelector。请参见清单 7。

SelectTemplateCore 方法尝试检索一个名称与 IUserOptionTemplateName 属性匹配的模板。使用一个缓存(一个 Dictionary<string, NamedTemplate>)来高效地 O(1) 检索模板。

清单 7. OptionTemplateSelector 类

public class OptionTemplateSelector : DataTemplateSelector
{
	public NamedTemplateCollection Templates { get; set; }

	IDictionary<string, NamedTemplate> cache { get; set; }

	void InitTemplateCollection()
	{
		cache = Templates?.ToDictionary(x => x.Name) 
							?? new Dictionary<string, NamedTemplate>();
	}

	protected override DataTemplate SelectTemplateCore(
					object item, DependencyObject container)
	{
		if (cache == null)
		{
			InitTemplateCollection();
		}

		if (item != null)
		{
			var readerWriter = (IUserOptionReaderWriter)item;
			var templateName = readerWriter.UserOption.TemplateName;

			cache.TryGetValue(templateName, out NamedTemplate keyedTemplate);
			
			if (keyedTemplate != null)
			{
				return keyedTemplate.DataTemplate;
			}
		}

		DataTemplate result = base.SelectTemplateCore(item, container);
		return result;
	}
}

回到 OptionsPage.xml 文件,我们看到选项呈现在 ListView 中。请参见清单 8。ListView 绑定到 CollectionViewSource 以检索其选项分组,而 OptionTemplateSelector 为每个选项检索 DataTemplate 对象。

清单 8. 选项在 ListView 中呈现

<ListView ItemsSource="{Binding Source={StaticResource optionsViewSource}}"
		ItemTemplateSelector="{StaticResource optionTemplateSelector}"
		SelectionMode="None">
	<ListView.ItemContainerStyle>
		<Style TargetType="ListViewItem">
			<Setter Property="HorizontalContentAlignment" Value="Stretch" />
		</Style>
	</ListView.ItemContainerStyle>
</ListView>

示例应用程序显示其选项,如图所示

结论

应用程序的功能会随着时间的推移而增长和变化。在为应用程序构建设置屏幕时,明智的做法是将其设计成可以轻松地向屏幕添加设置,而无需花费时间重新进行用户界面设计。实现这一目标的一种方法是使用像 Codon FX 这样的第三方框架,它允许您只需一行代码即可向应用程序添加新的用户选项。

在本文中,您学习了如何配置 .NET Standard 项目和 UWP 应用以使用 Codon FX。您研究了定义包含应用程序中使用的设置的 AppSettings 类,以及如何公开其中一部分设置作为用户选项。您看到了如何为用户选项创建 DataTemplate 元素,以及如何使用数据模板集合在设置屏幕上呈现每个用户选项。

希望您觉得本文有用。如果是,那么如果您能评分或在下方留下反馈,我将不胜感激。

历史

2019 年 3 月

  • 首次发布
© . All rights reserved.