Creating a User-Friendly Application: Inform, Engage, and Empower
In the realm of software development, crafting an application that not only meets functional requirements but also delivers a seamless and intuitive user experience is paramount. The adage “what, when, where” encapsulates the essence of user-friendly design, emphasizing the need to inform users about what happened, what it means, and what they can do about it. Additionally, guiding users on where to get more information and how to get help is crucial for fostering a sense of engagement and empowerment.
What Happened & What It Means
When an exception occurs, providing users with clear and concise information about the issue is essential. This involves explaining the nature of the problem and its implications in a language that is accessible to non-technical users. By demystifying errors, users are less likely to feel frustrated and more inclined to understand the situation.
What Users Can Do About It
Equipping users with actionable steps or solutions in response to an issue empowers them to resolve problems independently or navigate them with confidence. Whether it’s a simple fix, a workaround, or a directive to seek further assistance, the goal is to make users feel in control of the situation.
Where to Get More Information
Offering resources for users to delve deeper into the issue at hand is another facet of a user-friendly application. This could be in the form of FAQs, help articles, or forums where users can seek advice from the community or support teams.
How to Get Help
Ensuring that users have easy access to support channels is vital. This may include contact information for customer service, links to support tickets, or live chat options. The key is to make the process of seeking help as straightforward as possible.
Moving to Production: User Approval for Data Collection
As applications transition closer to production, the collection of user data for diagnostic purposes becomes a sensitive matter. It is imperative to seek user consent before sending any information to the developers. This not only respects user privacy but also builds trust.
The Code
Overview
Implementing these features can be efficiently managed using .NET Maui control templates. This approach allows for a modular and scalable design, where different aspects of user interaction can be encapsulated and managed independently. I’ll go over the code in the following sections.
.NET Maui documentation for content templates can be fond here.
In my solution I created a folder called CustomerControls and add a class file for each customer control I am going to use in my project. In the styles folder under Resources I added a file called ControlTemplates.xaml. In this file I put all my ContentTemplates for my custom controls.
I have also added a class file under Data called ErrorDictionary
.
The CustomControls file ErrorPopUpView.cs contains the binding information for your control and is the base class for your custom control. While the ContraolTemplates.xaml is where you define the visualization of your control in XAML. The ErrorPopUpView.cs basically provides the data parameters that you pass into your control. IE your data bindings used by your control.
Error Dictionary
Let’s take a look at ErrorDictionary.cs first. The code is pretty simple at this point. It simply loads a list of error information from an embedded file in the project. At a future date I’ll update the code so that the information is pulled down from the cloud and cached in a local file. That way enhancements and updates to the error information can be made without the need to redeploy the app. The class ErrorDetails
row 16 below is the data that is defined for each error code, here is where you would extend this class to provide any additional information for your standard error popup. Line 48 is where you get the details for an error code and it provides for a default set of information if the error code is not found. Here you could log or send information back to the developer for unknown error codes to handle proactively.
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()
{
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;
}
}
}
Custom Control Class
In the custom control class you are basically creating the bindable properties that are unique to your control. Here I have created bindable properties for all the ErrorDetails
class properties plus properties that are unique to that particular error and are passed in from the viewmodel. The ErrorReason
and ErrorMessage
property along with the ErrorCode
are passed into the popup through bindable properties in the viewmodel.
Learning: I discovered that I needed to add the OnPropertyChanged
method to the properties. This is not shown in the Microsoft learn documentation. In the OnErrorCodeChanged
method is where I lookup the error code from the Error Dictionary and then set the values of the control from the 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));
}
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));
}
}
}
Custom Control Template
The visualation for the custom control is contained within a resource dictionary. I created a file in the resources folder under styles and added a line in App.XAML to reference this newly added XAML file so that the standard custom controls I develop are available app wide and are stored in the application resource dictionary.
<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>
I have chosen to use the DevExpress DXPopup control for my visualization. From this point you just create your popup visual like you would any other XAML content. The one thing that took me a minute to figure out is that the ShowPopup binding needed to be twoway so that you could actually close the popup. The other is note the use of TemplateBinding
in binding vs just the use of 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>
Showing The Popup
Your page content view XAML add the custom control. To show the error popup set the ShowErrorPopup
property to true. Setting ErrorCode
, ErrorMessage
, and ErrorReason
properties provide the specific information for the error popup to display along with the defined error information from the error dictionary.
<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" />
In your view model define the properties for the custom control. And then set the ShowErrorPopup
and the ErrorCode
when you encounter a handled or unhandled condition.
[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);
}
That wraps it up for this post.