V1.2 新构架

This commit is contained in:
Aigio Liu
2021-02-20 16:45:44 +08:00
parent c2d95a1833
commit 4d76decf73
343 changed files with 41333 additions and 4 deletions
@@ -0,0 +1,69 @@
using MessagePack;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
namespace System.Application
{
[DebuggerDisplay("{DebuggerDisplay(),nq}")]
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
public sealed class AppClientAttribute : Attribute
{
string DebuggerDisplay() => $"type: {type}";
readonly Type? type;
string? str;
[NotNull, DisallowNull] // C# 8 not null
public object? Value { get; private set; }
public AppClientAttribute(Type type, string str)
{
this.type = type;
this.str = str;
init();
}
#pragma warning disable IDE1006 // 命名样式
void init()
#pragma warning restore IDE1006 // 命名样式
{
ctor();
void ctor()
{
#if DEBUG
var stopwatch = Stopwatch.StartNew();
#endif
var bytes = str.Base64UrlDecodeToByteArray_Nullable();
Value = MessagePackSerializer.Deserialize(type, bytes);
str = null;
#if DEBUG
stopwatch.Stop();
Debug.WriteLine($"AppClientAttr init 耗时:{stopwatch.ElapsedMilliseconds}ms");
#endif
}
}
[return: MaybeNull]
public static T Get<T>(Assembly? assembly = null)
{
#if DEBUG
var stopwatch = Stopwatch.StartNew();
#endif
assembly ??= Assembly.GetCallingAssembly();
var attrs = assembly.GetCustomAttributes<AppClientAttribute>();
var type = typeof(T);
var attr = attrs.FirstOrDefault(x => x.Value != null && x.type == type);
T t = (attr != default && attr.Value is T value) ? value : default;
#if DEBUG
stopwatch.Stop();
Debug.WriteLine($"AppClientAttr Get<{type.Name}> 耗时:{stopwatch.ElapsedMilliseconds}ms");
#endif
return t;
}
}
}
@@ -0,0 +1,13 @@
namespace System.Application.Models
{
/// <summary>
/// 应用设置
/// </summary>
public interface IAppSettings
{
/// <summary>
/// https://appcenter.ms
/// </summary>
string? AppSecretVisualStudioAppCenter { get; set; }
}
}
@@ -0,0 +1,20 @@
namespace System.Application.Services
{
/// <summary>
/// 页面跳转导航服务
/// </summary>
public interface INavigationService
{
/// <summary>
/// 页面跳转
/// </summary>
/// <param name="type"></param>
/// <param name="flags"></param>
void Push(Type type, PushFlags flags = PushFlags.Empty);
/// <summary>
/// 后退
/// </summary>
void Pop();
}
}
@@ -0,0 +1,49 @@
namespace System.Application.Services
{
/// <summary>
/// 通知栏服务
/// </summary>
/// <typeparam name="TNotificationType"></typeparam>
public interface INotificationService<TNotificationType> where TNotificationType : Enum
{
/// <summary>
/// 获取是否有通知权限
/// </summary>
/// <returns></returns>
bool AreNotificationsEnabled();
/// <summary>
/// 显示通知
/// </summary>
/// <param name="text">通知内容</param>
/// <param name="notificationType">通知类型</param>
/// <param name="autoCancel"></param>
/// <param name="title">通知标题</param>
/// <param name="entrance"></param>
void Notify(
string text,
TNotificationType notificationType,
bool autoCancel = true,
string? title = default,
Type? entrance = default);
/// <summary>
/// 取消通知
/// </summary>
/// <param name="notificationType"></param>
void Cancel(TNotificationType notificationType);
/// <summary>
/// 取消所有通知
/// </summary>
void CancelAll();
/// <summary>
/// 下载进度通知
/// </summary>
Progress<float> NotifyDownload(
string text,
TNotificationType notificationType,
string? title = default);
}
}
@@ -0,0 +1,36 @@
using System.Threading.Tasks;
using Xamarin.Essentials;
namespace System.Application.Services
{
/// <summary>
/// 运行时权限
/// </summary>
public interface IPermissions
{
/// <inheritdoc cref="Permissions.BasePlatformPermission"/>
public interface IPermission
{
/// <inheritdoc cref="Permissions.BasePlatformPermission.CheckStatusAsync"/>
Task<PermissionStatus> CheckStatusAsync();
/// <inheritdoc cref="Permissions.BasePlatformPermission.RequestAsync"/>
Task<PermissionStatus> RequestAsync();
/// <inheritdoc cref="Permissions.BasePlatformPermission.ShouldShowRationale"/>
bool ShouldShowRationale();
}
/// <summary>
/// 获取手机号码所需权限
/// </summary>
public interface IGetPhoneNumber : IPermission { }
/// <summary>
/// 检查并申请一组权限
/// </summary>
/// <param name="permission"></param>
/// <returns></returns>
Task<PermissionStatus> CheckAndRequestAsync(IPermission permission);
}
}
@@ -0,0 +1,16 @@
using System.Threading.Tasks;
namespace System.Application.Services
{
/// <summary>
/// 电话服务
/// </summary>
public interface ITelephonyService
{
/// <summary>
/// 获取当前设备的手机号码
/// </summary>
/// <returns></returns>
Task<string?> GetPhoneNumberAsync();
}
}
@@ -0,0 +1,18 @@
namespace System.Application.Services
{
/// <summary>
/// Toast属于一种轻量级的反馈,常常以小弹框的形式出现,一般出现1到2秒会自动消失,可以出现在屏幕上中下任意位置,但同个产品会模块尽量使用同一位置,让用户产生统一认知
/// </summary>
public interface IToast
{
/// <summary>
/// 显示Toast
/// </summary>
/// <param name="text"></param>
/// <param name="duration"></param>
void Show(string text, int? duration = null);
/// <inheritdoc cref="Show(string, int?)"/>
void Show(string text, ToastLength duration);
}
}
@@ -0,0 +1,15 @@
namespace System.Application.Services
{
/// <summary>
/// Toast显示拦截
/// </summary>
public interface IToastIntercept
{
/// <summary>
/// 显示Toast消息前拦截
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
bool OnShowExecuting(string text);
}
}
@@ -0,0 +1,10 @@
namespace System.Application.Services.Implementation
{
public sealed class NoneToastIntercept : IToastIntercept
{
bool IToastIntercept.OnShowExecuting(string text)
{
return false;
}
}
}
@@ -0,0 +1,62 @@
using System.Threading.Tasks;
using Xamarin.Essentials;
namespace System.Application.Services.Implementation
{
/// <inheritdoc cref="IPermissions"/>
public class PermissionsImpl : IPermissions
{
/// <summary>
/// 权限被拒绝,可提示用户在“设置”中启用,通常应有不再提示的选项
/// </summary>
/// <param name="permission"></param>
protected virtual void PromptUserGoToSettings(IPermissions.IPermission permission)
{
// 可由平台重写此函数实现,通常为显示一个UI上的弹窗,上有复选框不再显示
}
/// <summary>
/// 提示用户需要权限的其他信息
/// </summary>
/// <param name="permission"></param>
protected virtual void ShowRationale(IPermissions.IPermission permission)
{
// 可由平台重写此函数实现,通常为显示一个吐司消息,因调用此函数后会立即开始重新申请权限
}
public async Task<PermissionStatus> CheckAndRequestAsync(IPermissions.IPermission permission)
{
// https://docs.microsoft.com/zh-cn/xamarin/essentials/permissions?context=xamarin%2Fandroid&tabs=android#general-usage
var status = await permission.CheckStatusAsync();
if (status == PermissionStatus.Granted)
return status;
if (status == PermissionStatus.Denied && DeviceInfo.Platform == DevicePlatform.iOS)
{
// Prompt the user to turn on in settings
// 提示用户在“设置”中启用
// On iOS once a permission has been denied it may not be requested again from the application
// 在iOS上,一旦权限被拒绝,就不能再次从应用程序请求权限
PromptUserGoToSettings(permission);
return status;
}
if (permission.ShouldShowRationale())
{
// Prompt the user with additional information as to why the permission is needed
// 提示用户需要权限的其他信息
ShowRationale(permission);
}
status = await permission.RequestAsync();
if (status == PermissionStatus.Denied)
{
PromptUserGoToSettings(permission);
}
return status;
}
}
}
@@ -0,0 +1,43 @@
using System.Threading.Tasks;
namespace System.Application.Services.Implementation
{
/// <inheritdoc cref="ITelephonyService"/>
public abstract class TelephonyServiceImpl : ITelephonyService
{
readonly IPermissions pf;
readonly IPermissions.IGetPhoneNumber p1;
public TelephonyServiceImpl(IPermissions pf, IPermissions.IGetPhoneNumber p1)
{
this.pf = pf;
this.p1 = p1;
}
/// <summary>
/// 由特定平台实现的获取手机号码
/// </summary>
/// <returns></returns>
protected abstract string? PlatformGetPhoneNumber();
/// <summary>
/// 由特定平台实现的检查权限后获取手机号码
/// </summary>
/// <returns></returns>
protected virtual async Task<string?> PlatformGetPhoneNumberAsync()
{
var status = await pf.CheckAndRequestAsync(p1);
if (status.IsOk())
{
return PlatformGetPhoneNumber();
}
return null;
}
public async Task<string?> GetPhoneNumberAsync()
{
var phoneNumber = await PlatformGetPhoneNumberAsync();
return phoneNumber;
}
}
}
@@ -0,0 +1,70 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Xamarin.Essentials;
namespace System.Application.Services.Implementation
{
public abstract class ToastImpl : IToast
{
protected readonly IToastIntercept intercept;
protected const string TAG = "Toast";
public ToastImpl(IToastIntercept intercept)
{
this.intercept = intercept;
}
/// <summary>
/// 根据字符串长度计算持续时间
/// </summary>
/// <param name="len"></param>
/// <returns></returns>
protected virtual int CalcDurationByStringLength(int len)
=> len > 9 ? ToDuration(ToastLength.Long) : ToDuration(ToastLength.Short);
public void Show(string text, int? duration)
{
try
{
if (MainThread.IsMainThread)
{
Show_();
}
else
{
MainThread.BeginInvokeOnMainThread(Show_);
}
void Show_()
{
if (intercept.OnShowExecuting(text)) return;
var _duration = duration ?? CalcDurationByStringLength(text.Length);
PlatformShow(text, _duration);
}
}
catch (Exception e)
{
Log.Error(TAG, e, "ShowToast Error, text: {0}", text);
}
}
public void Show(string text, ToastLength duration) => Show(text, ToDuration(duration));
protected abstract void PlatformShow(string text, int duration);
/// <summary>
/// 将 <see cref="ToastLength"/> 转换为 持续时间
/// </summary>
/// <param name="toastLength"></param>
/// <returns></returns>
protected virtual int ToDuration(ToastLength toastLength) => (int)toastLength;
protected static IServiceCollection TryAddToast<TToastImpl>(IServiceCollection services) where TToastImpl : class, IToast
{
services.TryAddSingleton<IToastIntercept, NoneToastIntercept>();
services.TryAddSingleton<IToast, TToastImpl>();
return services;
}
}
}
@@ -0,0 +1,30 @@
namespace System.Application.Services
{
/// <summary>
/// 通知重要性级别
/// <para>Android: https://developer.android.google.cn/training/notify-user/channels#importance </para>
/// </summary>
public enum NotificationImportanceLevel : byte
{
/// <summary>
/// 低 没有声音,也没有出现在状态栏中。
/// </summary>
Low,
/// <summary>
/// 中等 没有声音
/// </summary>
Medium,
/// <summary>
/// 高 发出声音
/// </summary>
High,
/// <summary>
/// 紧急 发出声音,并显示为浮动通知
/// <para>浮动通知: 从 Android 5.0 开始,通知可以在称为浮动通知的浮动窗口中短暂显示。此行为通常针对用户应立即了解的重要通知,并且仅在设备解锁后才会出现。https://developer.android.google.cn/guide/topics/ui/notifiers/notifications?hl=zh-cn#Heads-up </para>
/// </summary>
Urgent,
}
}
@@ -0,0 +1,31 @@
namespace System.Application.Services
{
/// <summary>
/// 页面跳转的逻辑参数
/// </summary>
[Flags]
public enum PushFlags
{
Empty = 0,
/// <summary>
/// CurrentActivity.Finish()
/// </summary>
Finish = 0x00000001,
/// <summary>
/// ActivityFlags.ClearTop
/// </summary>
ClearTop = 0x00000010,
/// <summary>
/// ActivityFlags.SingleTop
/// </summary>
SingleTop = 0x00000100,
/// <summary>
/// CurrentActivity.OverridePendingTransition(0, 0)
/// </summary>
OverridePendingTransitionZeroZero = 0x00001000,
}
}
@@ -0,0 +1,119 @@
using System.Collections.Generic;
using System.Linq;
namespace System.Application.UI.Adapters
{
/// <summary>
/// 适配器
/// </summary>
/// <typeparam name="TViewModel"></typeparam>
public interface IAdapter<TViewModel> : ICreateViewModels<TViewModel>
{
/// <summary>
/// 视图模型
/// </summary>
IList<TViewModel> ViewModels { get; set; }
/// <summary>
/// 列表项点击事件
/// </summary>
event EventHandler<ItemClickEventArgs<TViewModel>>? ItemClick;
/// <summary>
/// 列表项【长按】点击事件
/// </summary>
event EventHandler<ItemClickEventArgs<TViewModel>>? ItemLongClick;
void NotifyDataSetChanged();
/// <summary>
/// 替换视图模型,默认替换后刷新
/// </summary>
/// <param name="newViewModels"></param>
/// <param name="notify"></param>
public void Replace(IEnumerable<TViewModel>? newViewModels, bool notify = true)
{
if (ViewModels.IsReadOnly)
{
if (newViewModels != null && newViewModels.Any())
{
ViewModels = CreateViewModels(newViewModels);
}
else
{
ViewModels = CreateViewModels();
}
}
else
{
ViewModels.Clear();
if (newViewModels != null && newViewModels.Any())
{
ViewModels.AddRange(newViewModels);
}
}
if (notify)
{
NotifyDataSetChanged();
}
}
/// <summary>
/// 添加新的视图模型,默认添加后刷新
/// </summary>
/// <param name="newViewModels"></param>
/// <param name="notify"></param>
public void AddRange(IEnumerable<TViewModel>? newViewModels, bool notify = true)
{
if (newViewModels != null && newViewModels.Any())
{
ViewModels.AddRange(newViewModels);
if (notify)
{
NotifyDataSetChanged();
}
}
}
/// <summary>
/// 根据下标移除视图模型,默认移除后刷新
/// </summary>
/// <param name="index"></param>
/// <param name="notify"></param>
public void RemoveAt(int index, bool notify = true)
{
if (index >= 0 && index < ViewModels.Count)
{
ViewModels.RemoveAt(index);
if (notify)
{
NotifyDataSetChanged();
}
}
}
/// <summary>
/// 根据值移除视图模型,默认移除后刷新
/// </summary>
/// <param name="vm"></param>
/// <param name="notify"></param>
public void Remove(TViewModel vm, bool notify = true)
{
if (ViewModels.Remove(vm))
{
if (notify)
{
NotifyDataSetChanged();
}
}
}
/// <summary>
/// 根据点击参数移除视图模型,默认移除后刷新
/// </summary>
/// <param name="args"></param>
/// <param name="notify"></param>
public void Remove(ItemClickEventArgs<TViewModel> args, bool notify = true)
=> RemoveAt(args.Position, notify);
}
}
@@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace System.Application.UI.Adapters
{
public interface ICreateViewModels<TViewModel>
{
public IList<TViewModel> CreateViewModels();
public IList<TViewModel> CreateViewModels(IEnumerable<TViewModel> newViewModels);
}
}
@@ -0,0 +1,23 @@
namespace System.Application.UI.Adapters
{
/// <summary>
/// 列表项点击事件参数
/// </summary>
/// <typeparam name="TViewModel"></typeparam>
#pragma warning disable IDE1006 // 命名样式
public interface ItemClickEventArgs<TViewModel>
#pragma warning restore IDE1006 // 命名样式
{
/// <summary>
/// 位置
/// </summary>
int Position { get; }
/// <summary>
/// 当前项
/// </summary>
TViewModel Current { get; }
bool Handled { get; set; }
}
}
@@ -0,0 +1,96 @@
using Microsoft.Extensions.Logging;
using System.Text;
namespace System.Logging
{
/// <summary>
/// 客户端日志
/// <para>https://github.com/dotnet/extensions/blob/v3.1.5/src/Logging/Logging.Console/src/ConsoleLogger.cs</para>
/// </summary>
public abstract class ClientLogger : ILogger
{
static readonly string _messagePadding;
static readonly string _newLineWithMessagePadding;
static ClientLogger()
{
_messagePadding = new string(' ', 6);
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
}
protected readonly string name;
public ClientLogger(string name) => this.name = name;
public virtual IDisposable BeginScope<TState>(TState state)
{
return NullScope.Instance;
}
public virtual bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
if (formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}
var message = formatter(state, exception);
if (!string.IsNullOrEmpty(message) || exception != null)
{
WriteMessage(logLevel, name, eventId.Id, message, exception);
}
}
public virtual void WriteMessage(LogLevel logLevel, string logName, int eventId, string? message, Exception? exception)
{
var logBuilder = new StringBuilder();
CreateDefaultLogMessage(logBuilder, logName, eventId, message, exception);
var logMessage = logBuilder.ToString();
WriteMessage(logLevel, logMessage);
}
void CreateDefaultLogMessage(StringBuilder logBuilder, string logName, int eventId, string? message, Exception? exception)
{
// Example:
// INFO: ConsoleApp.Program[10]
// Request received
// category and event id
logBuilder.Append(logName);
logBuilder.Append("[");
logBuilder.Append(eventId);
logBuilder.AppendLine("]");
if (!string.IsNullOrEmpty(message))
{
// message
logBuilder.Append(_messagePadding);
var len = logBuilder.Length;
logBuilder.AppendLine(message);
logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length);
}
// Example:
// System.InvalidOperationException
// at Namespace.Class.Function() in File:line X
if (exception != null)
{
// exception message
logBuilder.AppendLine(exception.ToString());
}
}
public abstract void WriteMessage(LogLevel logLevel, string message);
}
}
@@ -0,0 +1,22 @@
namespace System.Logging
{
/// <summary>
/// An empty scope without any logic
/// <para>https://github.com/dotnet/extensions/blob/v3.1.5/src/Logging/shared/NullScope.cs</para>
/// <para>https://github.com/dotnet/runtime/blob/v5.0.0-rtm.20519.4/src/libraries/Common/src/Extensions/Logging/NullScope.cs</para>
/// </summary>
public class NullScope : IDisposable
{
public static NullScope Instance { get; } = new NullScope();
private NullScope()
{
}
#pragma warning disable CA1816 // Dispose 方法应调用 SuppressFinalize
public void Dispose()
#pragma warning restore CA1816 // Dispose 方法应调用 SuppressFinalize
{
}
}
}
@@ -0,0 +1,61 @@
using System.Threading.Tasks;
using Xamarin.Essentials;
namespace System.Security
{
/// <summary>
/// https://docs.microsoft.com/zh-cn/xamarin/essentials/secure-storage
/// </summary>
internal sealed class ClientStorage : IStorage
{
string? IStorage.Get(string key)
{
// https://github.com/xamarin/Essentials/blob/1.5.3.2/Xamarin.Essentials/SecureStorage/SecureStorage.android.cs#L74
// https://github.com/xamarin/Essentials/blob/1.5.3.2/Xamarin.Essentials/SecureStorage/SecureStorage.ios.tvos.watchos.cs#L26
// https://github.com/xamarin/Essentials/blob/1.5.3.2/Xamarin.Essentials/SecureStorage/SecureStorage.uwp.cs#L23
Func<Task<string?>> func = () => SecureStorage.GetAsync(key);
var result = func.RunSync();
return result;
}
Task<string?> IStorage.GetAsync(string key)
{
return SecureStorage.GetAsync(key);
}
void IStorage.Set(string key, string? value)
{
// https://github.com/xamarin/Essentials/blob/1.5.3.2/Xamarin.Essentials/SecureStorage/SecureStorage.android.cs#L93
// https://github.com/xamarin/Essentials/blob/1.5.3.2/Xamarin.Essentials/SecureStorage/SecureStorage.ios.tvos.watchos.cs#L34
// https://github.com/xamarin/Essentials/blob/1.5.3.2/Xamarin.Essentials/SecureStorage/SecureStorage.uwp.cs#L37
if (string.IsNullOrEmpty(value))
{
SecureStorage.Remove(key);
return;
}
Func<Task> func = () => SecureStorage.SetAsync(key, value);
func.RunSync();
}
Task IStorage.SetAsync(string key, string? value)
{
if (string.IsNullOrEmpty(value))
{
SecureStorage.Remove(key);
return Task.CompletedTask;
}
return SecureStorage.SetAsync(key, value);
}
bool IStorage.Remove(string key)
{
return SecureStorage.Remove(key);
}
Task<bool> IStorage.RemoveAsync(string key)
{
var result = SecureStorage.Remove(key);
return Task.FromResult(result);
}
}
}
@@ -0,0 +1,33 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using System.Application.Services;
using System.Application.Services.Implementation;
using System.Security;
// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection
{
public static class ServiceCollectionExtensions
{
/// <summary>
/// 添加由 Xamarin.Essentials.SecureStorage 实现的 <see cref="IStorage"/>
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection TryAddStorage(this IServiceCollection services)
{
services.TryAddSingleton<IStorage, ClientStorage>();
return services;
}
/// <summary>
/// 添加运行时权限
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection TryAddPermissions(this IServiceCollection services)
{
services.TryAddSingleton<IPermissions, PermissionsImpl>();
return services;
}
}
}
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<LangVersion>9.0</LangVersion>
<Nullable>enable</Nullable>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<SignAssembly>true</SignAssembly>
<DelaySign>false</DelaySign>
<AssemblyOriginatorKeyFile>..\Unicorn.pfx</AssemblyOriginatorKeyFile>
<RootNamespace>System</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\System.Common.CoreLib\Properties\AssemblyInfo.cs">
<Link>Properties\AssemblyInfo.cs</Link>
</Compile>
<Compile Include="..\System.Common.CoreLib\Properties\InternalsVisibleTo.cs">
<Link>Properties\InternalsVisibleTo.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\System.Application.Repositories.sqlite-net-pcl\System.Application.Repositories.sqlite-net-pcl.csproj" />
<ProjectReference Include="..\System.Common.CoreLib\System.Common.CoreLib.csproj" />
</ItemGroup>
</Project>
+22
View File
@@ -0,0 +1,22 @@
using System.Application.Services;
namespace System
{
/// <inheritdoc cref="IToast"/>
public static class Toast
{
/// <inheritdoc cref="IToast.Show(string, int?)"/>
public static void Show(string text, int? duration = null)
{
var toast = DI.Get<IToast>();
toast.Show(text, duration);
}
/// <inheritdoc cref="IToast.Show(string, ToastLength)"/>
public static void Show(string text, ToastLength duration)
{
var toast = DI.Get<IToast>();
toast.Show(text, duration);
}
}
}
+17
View File
@@ -0,0 +1,17 @@
// Length对应的毫秒数值参考文章 https://www.cnblogs.com/kborid/p/14078744.html
namespace System
{
public enum ToastLength
{
/// <summary>
/// Show the view or text notification for a short period of time.
/// </summary>
Short = 2000,
/// <summary>
/// Show the view or text notification for a long period of time.
/// </summary>
Long = 3500,
}
}