.NET MAUI 标准错误弹出窗口模式
本文提供了一份综合指南,旨在帮助开发人员通过信息丰富的错误处理和用户赋权功能来增强应用程序的用户体验。
打造用户友好的应用程序:告知、参与和赋能
在软件开发领域,打造一个不仅满足功能需求,还能提供无缝、直观用户体验的应用程序至关重要。“什么、何时、何地”的格言概括了用户友好设计的精髓,强调需要告知用户发生了什么、这意味着什么以及他们可以采取什么行动。此外,指导用户在哪里获取更多信息以及如何获得帮助,对于培养参与感和赋能感至关重要。
发生了什么 & 这意味着什么
当出现异常时,向用户提供有关问题的清晰简洁的信息至关重要。这包括以非技术用户能够理解的语言解释问题的性质及其影响。通过揭开错误的神秘面纱,用户不太可能感到沮丧,而更倾向于理解情况。
用户可以采取什么措施
让用户能够针对问题采取可操作的步骤或解决方案,从而使他们能够独立解决问题或自信地应对问题。无论是简单的修复、变通方法还是寻求进一步帮助的指令,目标都是让用户感觉能够掌控局面。
在哪里获取更多信息
提供资源供用户深入了解当前问题是用户友好应用程序的另一个方面。这可以采用常见问题解答、帮助文章或用户可以寻求社区或支持团队建议的论坛等形式。
如何获得帮助
确保用户能够轻松访问支持渠道至关重要。这可能包括客户服务的联系信息、支持票证的链接或实时聊天选项。关键是使寻求帮助的过程尽可能简单明了。
转向生产:用户对数据收集的批准
随着应用程序越来越接近生产阶段,出于诊断目的收集用户数据成为一个敏感问题。在将任何信息发送给开发人员之前,必须征得用户同意。这不仅尊重用户隐私,还能建立信任。
代码
概述
使用 .NET Maui 控件模板可以有效地管理这些功能的实现。这种方法允许模块化和可扩展的设计,其中用户交互的不同方面可以被封装并独立管理。我将在接下来的几节中介绍代码。
有关 .NET Maui 内容模板的文档可在 此处找到。
在我的解决方案中,我创建了一个名为“CustomerControls”的文件夹,并在其中为我将在项目中使用的每个自定义控件添加了一个类文件。在 Resources 下的 Styles 文件夹中,我添加了一个名为 ControlTemplates.xaml 的文件。在此文件中,我放置了所有自定义控件的内容模板。
我还向 Data 下添加了一个名为 `ErrorDictionary` 的类文件。
CustomControls 文件 `ErrorPopUpView.cs` 包含控件的绑定信息,是自定义控件的基类。而 `ContraolTemplates.xaml` 是您在 XAML 中定义控件可视化的地方。`ErrorPopUpView.cs` 主要提供您传递到控件的数据参数。即您的控件使用的数据绑定。
错误字典
让我们先看看 `ErrorDictionary.cs`。目前的代码非常简单。它只是从项目中的嵌入文件中加载错误信息的列表。将来,我将更新代码,以便信息可以从云端下载并缓存在本地文件中。这样,无需重新部署应用程序即可对错误信息进行增强和更新。第 16 行的 `ErrorDetails` 类是为每个错误代码定义的数据,您可以在此处扩展此类以提供标准错误弹出窗口的任何附加信息。第 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` 类的属性创建了可绑定属性,此外还为该特定错误独有的属性创建了可绑定属性,这些属性通过 ViewModel 传入。 `ErrorReason` 和 `ErrorMessage` 属性以及 `ErrorCode` 通过 ViewModel 中的可绑定属性传入弹出窗口。
学习:我发现我需要在属性中添加 `OnPropertyChanged` 方法。这在 Microsoft 学习文档中没有显示。在 `OnErrorCodeChanged` 方法中,我从 Error Dictionary 中查找错误代码,然后从字典中设置控件的值。
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
` 而不是简单的 `binding`。
<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" />
在您的 ViewModel 中,为自定义控件定义属性。然后,当遇到已处理或未处理的条件时,设置 `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);
}
本帖到此结束。