目录
创建用户友好的应用程序:通知、参与和授权
在软件开发领域,精心设计一个既能满足功能要求,又能提供无缝和直观的用户体验的应用程序至关重要。“什么,何时,何地”这句格言概括了用户友好设计的本质,强调需要告知用户发生了什么,这意味着什么,以及他们可以做些什么。此外,指导用户从哪里获得更多信息以及如何获得帮助对于培养参与感和赋权感至关重要。
发生了什么,意味着什么
当发生异常时,向用户提供有关问题的清晰简洁的信息至关重要。这涉及用非技术用户可以访问的语言解释问题的性质及其影响。通过揭开错误的神秘面纱,用户不太可能感到沮丧,更倾向于理解情况。
用户可以做些什么
为用户提供可操作的步骤或解决方案来响应问题,使他们能够独立解决问题或自信地解决问题。无论是简单的修复、解决方法还是寻求进一步帮助的指令,目标都是让用户感到可以控制情况。
从何处获取更多信息
为用户提供资源以深入研究手头的问题是用户友好型应用程序的另一个方面。这可以采用常见问题解答、帮助文章或论坛的形式,用户可以在其中向社区或支持团队寻求建议。
如何获得帮助
确保用户能够轻松访问支持渠道至关重要。这可能包括客户服务的联系信息、支持票证的链接或实时聊天选项。关键是要使寻求帮助的过程尽可能简单。
迁移到生产环境:用户批准数据收集
随着应用程序向生产环境过渡,出于诊断目的收集用户数据成为一个敏感问题。在向开发人员发送任何信息之前,必须征得用户的同意。这不仅尊重了用户的隐私,还建立了信任。
守则
概述
可以使用.NET Maui控件模板有效地管理实现这些功能。这种方法允许模块化和可扩展的设计,其中用户交互的不同方面可以独立封装和管理。我将在以下各节中介绍这些代码。
- 错误字典
- 自定义控件类
- 自定义控件模板
- 显示弹出窗口
内容模板的.NET Maui文档可以在这里得到喜欢。
在我的解决方案中,我创建了一个名为CustomerControls的文件夹,并为我将在项目中使用的每个客户控件添加一个类文件。在“资源”下的“样式”文件夹中,我添加了一个名为ControlTemplates.xaml的文件。在此文件中,我将所有ContentTemplates用于我的自定义控件。
我还在Data下添加了一个名为ErrorDictionary的类文件。
CustomControls文件ErrorPopUpView.cs包含控件的绑定信息,并且是自定义控件的基类。而 ContraolTemplates.xaml 是在XAML中定义控件的可视化效果的位置。ErrorPopUpView.cs基本上提供传递到控件中的数据参数。IE控件使用的数据绑定。
错误字典
让我们先来看看ErrorDictionary.cs。在这一点上,代码非常简单。它只是从项目中的嵌入文件中加载错误信息列表。在将来的某个日期,我将更新代码,以便从云中提取信息并缓存在本地文件中。这样,就可以对错误信息进行增强和更新,而无需重新部署应用程序。下面的ErrorDetails类行16是为每个错误代码定义的数据,您可以在此处扩展此类以为您的标准错误弹出窗口提供任何其他信息。第48行是获取错误代码详细信息的位置,如果未找到错误代码,它会提供一组默认信息。在这里,您可以记录或将信息发送回开发人员,以便主动处理未知错误代码。
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.Maui.Storage;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MyNextBook.Models
{
public class ErrorDetails
{
public string ErrorCode;
public string ErrorTitle;
public string WhatThisMeans;
public string WhatYouCanDo;
public string HelpLink;
public string Type;
}
static public class ErrorDictionary
{
static ErrorDictionary()
{
LoadErrorsFromFile();
}
static public List<ErrorDetails> Errors { get; set; }
static public void LoadErrorsFromFile()
{
//string path = "Resources/Raw/ErrorDetails.json";
var stream = FileSystem.OpenAppPackageFileAsync("ErrorDetailsList.json").Result;
var reader = new StreamReader(stream);
var contents = reader.ReadToEnd();
Errors = JsonConvert.DeserializeObject<List<ErrorDetails>>(contents);
}
static public ErrorDetails GetErrorDetails(string errorCode)
{
var error = Errors.FirstOrDefault(e => e.ErrorCode == errorCode);
if (error == null)
{
ErrorDetails errorDetail = new ErrorDetails
{
ErrorCode = "mnb-999",
ErrorTitle = "Error code not defined: " + errorCode,
WhatThisMeans = "The error code is not in the list of know error codes. More than likely this is a developer problem and will be resolved with an upcoming release",
WhatYouCanDo = "If you have opted in to share analytics and errors with the developer data related to this situation will be provided to the developer so that they can provide a fix with a future release",
HelpLink = "http://helpsite.com/error1"
};
}
return error;
}
}
}
自定义控件类
在自定义控件类中,基本上是创建控件所特有的可绑定属性。在这里,我为所有ErrorDetails类属性以及该特定错误所特有的属性创建了可绑定属性,这些属性是从视图模型传入的。ErrorReason和ErrorMessage属性和ErrorCode通过视图模型中的可绑定属性传递到弹出窗口中。
学习:我发现我需要将该OnPropertyChanged方法添加到属性中。Microsoft Learn文档中未显示此内容。在该OnErrorCodeChanged方法中,我从错误字典中查找错误代码,然后从字典中设置控件的值。
using CommunityToolkit.Maui.Views;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DevExpress.Maui.Charts;
using DevExpress.Maui.Controls;
using DevExpress.Maui.Core.Internal;
using DevExpress.Utils.Filtering.Internal;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls;
using MyNextBook.Helpers;
using MyNextBook.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MyNextBook.CustomControls
{
public partial class ErrorPopupView : ContentView, INotifyPropertyChanged
{
public static readonly BindableProperty ErrorTitleProperty = BindableProperty.Create(nameof(ErrorTitle), typeof(string), typeof(ErrorPopupView), propertyChanged: OnErrorTitleChanged);
public static readonly BindableProperty ShowErrorPopupProperty = BindableProperty.Create(nameof(ShowErrorPopup), typeof(bool), typeof(ErrorPopupView), propertyChanged: OnShowErrorPopupChanged);
public static readonly BindableProperty ErrorMessageProperty = BindableProperty.Create(nameof(ErrorMessage), typeof(string), typeof(ErrorPopupView), propertyChanged: OnErrorMessageChanged);
public static readonly BindableProperty ErrorCodeProperty = BindableProperty.Create(nameof(ErrorCode), typeof(string), typeof(ErrorPopupView), propertyChanged: OnErrorCodeChanged);
public static readonly BindableProperty ErrorReasonProperty = BindableProperty.Create(nameof(ErrorReason), typeof(string), typeof(ErrorPopupView), propertyChanged: OnErrorReasonChanged);
public static readonly BindableProperty WhatThisMeansProperty = BindableProperty.Create(nameof(WhatThisMeans), typeof(string), typeof(ErrorPopupView), propertyChanged: OnWhatThisMeansChanged);
public static readonly BindableProperty WhatYouCanDoProperty = BindableProperty.Create(nameof(WhatYouCanDo), typeof(string), typeof(ErrorPopupView), propertyChanged: OnWhatYouCanDoChanged);
public static readonly BindableProperty HelpLinkProperty = BindableProperty.Create(nameof(HelpLink), typeof(string), typeof(ErrorPopupView), propertyChanged: OnHelpLinkChanged);
public static readonly BindableProperty ErrorTypeProperty = BindableProperty.Create(nameof(ErrorType), typeof(string), typeof(ErrorPopupView), propertyChanged: OnErrorTypeChanged);
public bool ShowInfo { get; set; } = false;
public bool ShowErrorCode { get; set; } = true;
public string ErrorType { get; set; }
public string ExpanderIcon { get; set; } = IconFont.ChevronDown;
public bool ErrorMoreExpanded { get; set; } = false;
[RelayCommand] void ClosePopUp() => ShowErrorPopup = false;
[RelayCommand]
void ToggleErrorMore()
{
ExpanderIcon = (ExpanderIcon == IconFont.ChevronDown) ? IconFont.ChevronUp : IconFont.ChevronDown;
ErrorMoreExpanded = !ErrorMoreExpanded;
OnPropertyChanged(nameof(ErrorMoreExpanded));
OnPropertyChanged(nameof(ExpanderIcon));
}
private void Popup_Closed(object sender, EventArgs e)
{
ErrorHandler.AddLog("do something here");
}
[RelayCommand]
public void OpenHelpLink(string url)
{
if (url != null)
{
Launcher.OpenAsync(new Uri(url));
}
}
public string ErrorTitle
{
get => (string)GetValue(ErrorTitleProperty);
set => SetValue(ErrorTitleProperty, value);
}
private static void OnErrorTypeChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (ErrorPopupView)bindable;
control.ErrorType = (string)newValue;
control.OnPropertyChanged(nameof(ErrorType));
switch (control.ErrorType)
{
case "Info":
control.ShowErrorCode = false;
control.ShowInfo = true;
break;
case "Error":
control.ShowErrorCode = true;
control.ShowInfo = false;
break;
default:
control.ShowErrorCode = true;
control.ShowInfo = false;
break;
}
}
private static void OnErrorTitleChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (ErrorPopupView)bindable;
control.ErrorTitle = (string)newValue;
control.OnPropertyChanged(nameof(ErrorTitle));
}
public bool ShowErrorPopup
{
get => (bool)GetValue(ShowErrorPopupProperty);
set => SetValue(ShowErrorPopupProperty, value);
}
private static void OnShowErrorPopupChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (ErrorPopupView)bindable;
control.ShowErrorPopup = (bool)newValue;
control.OnPropertyChanged(nameof(ShowErrorPopup));
//ErrorHandler.AddLog("ErrorPopupView ShowErrorPopup: " + newValue);
}
public string ErrorMessage
{
get => (string)GetValue(ErrorMessageProperty);
set => SetValue(ErrorMessageProperty, value);
}
private static void OnErrorMessageChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (ErrorPopupView)bindable;
control.ErrorMessage = (string)newValue;
control.OnPropertyChanged(nameof(ErrorMessage));
}
public string ErrorCode
{
get => (string)GetValue(ErrorCodeProperty);
set => SetValue(ErrorCodeProperty, value);
}
private static void OnErrorCodeChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (ErrorPopupView)bindable;
control.ErrorCode = (string)newValue;
control.OnPropertyChanged(nameof(ErrorCode));
ErrorDetails ed = ErrorDictionary.GetErrorDetails(control.ErrorCode);
if (ed != null)
{
control.ErrorTitle = ed.ErrorTitle;
control.WhatThisMeans = ed.WhatThisMeans;
control.WhatYouCanDo = ed.WhatYouCanDo;
control.HelpLink = ed.HelpLink;
control.ErrorType = ed.Type;
control.OnPropertyChanged(nameof(ErrorTitle));
control.OnPropertyChanged(nameof(WhatThisMeans));
control.OnPropertyChanged(nameof(WhatYouCanDo));
control.OnPropertyChanged(nameof(HelpLink));
control.OnPropertyChanged(nameof (ErrorType));
switch (control.ErrorType)
{
case "Info":
control.ShowErrorCode = false;
control.ShowInfo = true;
break;
case "Error":
control.ShowErrorCode = true;
control.ShowInfo = false;
break;
default:
control.ShowErrorCode = true;
control.ShowInfo = false;
break;
}
}
}
public string ErrorReason
{
get => (string)GetValue(ErrorReasonProperty);
set => SetValue(ErrorReasonProperty, value);
}
private static void OnErrorReasonChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (ErrorPopupView)bindable;
control.ErrorReason = (string)newValue;
control.OnPropertyChanged(nameof(ErrorReason));
}
public string WhatThisMeans
{
get => (string)GetValue(WhatThisMeansProperty);
set => SetValue(WhatThisMeansProperty, value);
}
private static void OnWhatThisMeansChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (ErrorPopupView)bindable;
control.WhatThisMeans = (string)newValue;
control.OnPropertyChanged(nameof(WhatThisMeans));
}
public string WhatYouCanDo
{
get => (string)GetValue(WhatYouCanDoProperty);
set => SetValue(WhatYouCanDoProperty, value);
}
private static void OnWhatYouCanDoChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (ErrorPopupView)bindable;
control.WhatYouCanDo = (string)newValue;
control.OnPropertyChanged(nameof(WhatYouCanDo));
}
public string HelpLink
{
get => (string)GetValue(HelpLinkProperty);
set => SetValue(HelpLinkProperty, value);
}
private static void OnHelpLinkChanged(BindableObject bindable, object oldValue, object newValue)
{
var control = (ErrorPopupView)bindable;
control.HelpLink = (string)newValue;
control.OnPropertyChanged(nameof(HelpLink));
}
}
}
自定义控件模板
自定义控件的可视化效果包含在资源字典中。我在resources文件夹的styles下创建了一个文件,并在App.XAML中添加了一行来引用这个新添加的XAML文件,以便我开发的标准自定义控件在应用范围内可用,并存储在应用程序资源字典中。
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
<ResourceDictionary Source="Resources/Styles/ControlTemplates.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
我选择使用DevExpress DXPopup控件进行可视化。从这一点开始,只需创建弹出窗口视觉对象,就像创建任何其他XAML内容一样。我花了一分钟才弄清楚的一件事是ShowPopup绑定需要是双向的,以便您可以实际关闭弹出窗口。另一个是注意在绑定中使用 TemplateBinding 与仅使用绑定。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:dx="clr-namespace:DevExpress.Maui.Core;assembly=DevExpress.Maui.Core"
xmlns:dxco="clr-namespace:DevExpress.Maui.Controls;assembly=DevExpress.Maui.Controls"
xmlns:markups="clr-namespace:OnScreenSizeMarkup.Maui;assembly=OnScreenSizeMarkup.Maui"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit">
<ControlTemplate x:Key="SimilarSeries" />
<ControlTemplate x:Key="ErrorPopupStandard">
<dxco:DXPopup
x:Name="ErrorPopup"
AllowScrim="False"
Background="White"
BackgroundColor="White"
CornerRadius="20"
IsOpen="{TemplateBinding ShowErrorPopup,
Mode=TwoWay}">
<Grid
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
HeightRequest="500"
RowDefinitions="60,*,50"
WidthRequest="300">
<Border
Grid.Row="0"
Margin="6,5,5,15"
BackgroundColor="{dx:ThemeColor ErrorContainer}"
HorizontalOptions="FillAndExpand"
IsVisible="{TemplateBinding ShowErrorCode}"
Stroke="{dx:ThemeColor Outline}"
StrokeShape="RoundRectangle 30"
StrokeThickness="3">
<Label
Margin="0,4,0,4"
BackgroundColor="{dx:ThemeColor ErrorContainer}"
HorizontalOptions="Center"
Text="{TemplateBinding ErrorTitle}"
TextColor="{dx:ThemeColor Error}" />
</Border>
<Border
Grid.Row="0"
Margin="6,5,5,15"
BackgroundColor="{dx:ThemeColor ErrorContainer}"
HorizontalOptions="FillAndExpand"
IsVisible="{TemplateBinding ShowInfo}"
Stroke="{dx:ThemeColor Outline}"
StrokeShape="RoundRectangle 15"
StrokeThickness="3">
<Label
Margin="0,4,0,4"
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
HorizontalOptions="Center"
Text="{TemplateBinding ErrorTitle}"
TextColor="{dx:ThemeColor OnSecondaryContainer}" />
</Border>
<ScrollView Grid.Row="1" Margin="0,0,0,10">
<VerticalStackLayout Margin="6,0,4,10">
<Label
Margin="1,0,0,1"
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
HorizontalOptions="Start"
IsVisible="{TemplateBinding ShowErrorCode}"
LineBreakMode="WordWrap"
Text="{TemplateBinding ErrorCode,
StringFormat='Error Code: {0}'}"
TextColor="Black" />
<Label
Margin="1,0,0,5"
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
HorizontalOptions="Start"
LineBreakMode="WordWrap"
Text="{TemplateBinding ErrorReason}"
TextColor="{dx:ThemeColor OnErrorContainer}" />
<toolkit:Expander
x:Name="ErrorDetailExpander"
Margin="5,0,5,20"
IsExpanded="{TemplateBinding ErrorMoreExpanded}">
<toolkit:Expander.Header>
<VerticalStackLayout>
<Label
Margin="0,0,0,1"
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
HorizontalOptions="Start"
Text="What this means:"
TextColor="Black" />
<Label
Margin="0,0,0,5"
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
FontSize="Micro"
HorizontalOptions="Start"
LineBreakMode="WordWrap"
Text="{TemplateBinding WhatThisMeans}"
TextColor="Black" />
<Label
Margin="0,0,0,1"
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
HorizontalOptions="Start"
Text="What you can do:"
TextColor="Black" />
<Label
Margin="0,0,0,5"
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
FontSize="Micro"
HorizontalOptions="Start"
LineBreakMode="WordWrap"
Text="{TemplateBinding WhatYouCanDo}"
TextColor="Black" />
<HorizontalStackLayout>
<Label
Margin="0,0"
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
FontAttributes="Bold"
FontSize="Small"
Text="More Details"
TextColor="{dx:ThemeColor OnPrimaryContainer}"
VerticalOptions="Center" />
<ImageButton
Aspect="Center"
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
Command="{TemplateBinding ToggleErrorMoreCommand}"
CornerRadius="20"
HeightRequest="38"
HorizontalOptions="Center"
VerticalOptions="Center"
WidthRequest="38">
<ImageButton.Source>
<FontImageSource
x:Name="ErrorExpanderGlyph"
FontFamily="MD"
Glyph="{TemplateBinding ExpanderIcon}"
Size="24"
Color="{dx:ThemeColor OnPrimaryContainer}" />
</ImageButton.Source>
</ImageButton>
</HorizontalStackLayout>
</VerticalStackLayout>
</toolkit:Expander.Header>
<VerticalStackLayout>
<Label
Margin="0,0,0,1"
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
HorizontalOptions="Start"
Text="Error Message"
TextColor="Black" />
<Label
BackgroundColor="{dx:ThemeColor SecondaryContainer}"
FontSize="Micro"
HorizontalOptions="Start"
LineBreakMode="WordWrap"
Text="{TemplateBinding ErrorMessage}"
TextColor="Black" />
</VerticalStackLayout>
</toolkit:Expander>
</VerticalStackLayout>
</ScrollView>
<Button
Grid.Row="2"
Command="{TemplateBinding ClosePopUpCommand}"
Text="Close" />
</Grid>
</dxco:DXPopup>
</ControlTemplate>
<ControlTemplate x:Key="ShortBookDetailView">
<VerticalStackLayout>
<Label Text="{TemplateBinding BookTitle}" TextColor="Green" />
<Border
Margin="5,0,5,5"
BackgroundColor="red"
HorizontalOptions="FillAndExpand"
Stroke="{dx:ThemeColor Outline}"
StrokeShape="RoundRectangle 30"
StrokeThickness="3">
<Border.Shadow>
<Shadow
Brush="White"
Opacity=".25"
Radius="10"
Offset="10,5" />
</Border.Shadow>
<VerticalStackLayout>
<Label Text="Hello" TextColor="Black" />
<Label Text="{TemplateBinding BookTitle}" TextColor="Black" />
</VerticalStackLayout>
</Border>
</VerticalStackLayout>
</ControlTemplate>
</ResourceDictionary>
显示弹出窗口
页面内容视图XAML添加自定义控件。若要显示错误弹出窗口,请将该ShowErrorPopup属性设置为true。设置ErrorCode、ErrorMessage和ErrorReason属性提供要显示的错误弹出窗口的特定信息以及错误字典中定义的错误信息。
<controls:ErrorPopupView
Grid.Row="0"
Grid.Column="0"
ControlTemplate="{StaticResource ErrorPopupStandard}"
ErrorCode="{Binding ErrorCode}"
ErrorMessage="{Binding ErrorMessage}"
ErrorReason="{Binding ErrorReason}"
HeightRequest="1"
ShowErrorPopup="{Binding ShowErrorPopup}"
WidthRequest="1" />
在视图模型中,定义自定义控件的属性。然后设置ShowErrorPopup和ErrorCode当您遇到已处理或未处理的情况时。
[ObservableProperty] private bool showErrorPopup;
[ObservableProperty] private string errorTitle;
[ObservableProperty] private string errorMessage;
[ObservableProperty] private string errorCode;
[ObservableProperty] private string errorReason;
...
} catch (Exception ex)
{
ErrorCode = "MNB-000";
ErrorReason = ex.Message;
ErrorMessage = ex.ToString();
ShowErrorPopup = true;
ErrorHandler.AddError(ex);
}
这篇文章到此结束。
本文最初发表于 DOTNET MAUI Standard Error Popup Pattern – Brady Blogs
https://www.codeproject.com/Articles/5382609/NET-MAUI-Standard-Error-Popup-Pattern