10.完成出色的用户体验

提供用户反馈

一个伟大的应用程序的一个重要方面是让最终用户了解应用程序中正在发生的事情。 如果他们单击功能按钮,则应告知他们操作的进度或状态。 如果没有足够的反馈,用户可能会怀疑某个特定操作是否有效,并且可能会尝试多次运行它,这可能会导致错误。

因此,在我们的应用程序框架中实现反馈系统至关重要。 到目前为止,在本书中,我们已经在一些地方看到了 FeedbackManager 类的名称,尽管我们很少看到实现。 现在让我们看看如何在我们的应用程序框架中实现一个有效的反馈系统,从包含各个反馈消息的 Feedback 类开始:

using System;
using System.ComponentModel;
using CompanyName.ApplicationName.DataModels.Enums;
using CompanyName.ApplicationName.DataModels.Interfaces;
using CompanyName.ApplicationName.Extensions;
namespace CompanyName.ApplicationName.DataModels
{
   
    public class Feedback : IAnimatable, INotifyPropertyChanged
    {
   
        private string message = string.Empty;
        private FeedbackType type = FeedbackType.None;
        private TimeSpan duration = new TimeSpan(0, 0, 4);
        private bool isPermanent = false;
        private Animatable animatable;
        public Feedback(string message, FeedbackType type, TimeSpan duration)
        {
   
            Message = message;
            Type = type;
            Duration = duration == TimeSpan.Zero ? this.duration : duration;
            IsPermanent = false;
            Animatable = new Animatable(this);
        }
        public Feedback(string message, bool isSuccess, bool isPermanent) :
        this(message, isSuccess ? FeedbackType.Success :
             FeedbackType.Error, TimeSpan.Zero)
        {
   
            IsPermanent = isPermanent;
        }
        public Feedback(string message, FeedbackType type) : this(message,
                                                                  type, TimeSpan.Zero) {
    }
        public Feedback(string message, bool isSuccess) : this(message,
                                                               isSuccess ? FeedbackType.Success : FeedbackType.Error,
                                                               TimeSpan.Zero) {
    }
        public Feedback() : this(string.Empty, FeedbackType.None) {
    }
        public string Message
        {
   
            get {
    return message; }
            set {
    message = value; NotifyPropertyChanged(); }
        }
        public TimeSpan Duration
        {
   
            get {
    return duration; }
            set {
    duration = value; NotifyPropertyChanged(); }
        }
        public FeedbackType Type
        {
   
            get {
    return type; }
            set {
    type = value; NotifyPropertyChanged(); }
        }
        public bool IsPermanent
        {
   
            get {
    return isPermanent; }
            set {
    isPermanent = value; NotifyPropertyChanged(); }
        }
        #region IAnimatable Members
            public Animatable Animatable
        {
   
            get {
    return animatable; }
            set {
    animatable = value; }
        }
        #endregion
            #region INotifyPropertyChanged Members
            ...
            #endregion
    }
}

请注意,我们的 Feedback 类实现了我们之前看到的 IAnimatable 接口以及 INotifyPropertyChanged 接口。 在声明了私有字段之后,我们声明了一些有用的构造函数重载。

在此示例中,我们为持续时间字段硬编码了 4 秒的默认反馈显示持续时间。 在主构造函数中,我们根据持续时间输入参数的值设置 Duration 属性; 如果输入参数是 TimeSpan.Zero 字段,则使用默认值,但如果输入参数是非零值,则使用默认值。

Message 属性将保存反馈消息; Duration 属性指定消息显示的时间长度; Type 属性使用我们之前看到的 FeedbackType 枚举来指定消息的类型,而 IsPermanent 属性指示消息是否应该永久显示,直到用户手动关闭它。

我们的 IAnimatable 类的实现显示在其他属性下方,并且仅包含 Animatable 属性,但为了简洁起见,我们省略了 INotifyPropertyChanged 接口的实现,因为我们使用的是之前看到的默认实现。

现在让我们看看将包含各个反馈实例的 FeedbackCollection 类:

using System.Collections.Generic;
using System.Linq;
namespace CompanyName.ApplicationName.DataModels.Collections
{
   
    public class FeedbackCollection : BaseAnimatableCollection<Feedback>
    {
   
        public FeedbackCollection(IEnumerable<Feedback> feedbackCollection) :
        base(feedbackCollection) {
    }
        public FeedbackCollection() : base() {
    }
        public new void Add(Feedback feedback)
        {
   
            if (!string.IsNullOrEmpty(feedback.Message) && (Count == 0 ||
                                                            !this.Any(f => f.Message == feedback.Message))) base.Add(feedback);
        }
        public void Add(string message, bool isSuccess)
        {
   
            Add(new Feedback(message, isSuccess));
        }
    }
}

FeedbackCollection 类扩展了我们之前看到的 BaseAnimatableCollection 类,并将其泛型类型参数设置为 Feedback 类。 这是一个非常简单的类,并声明了几个构造函数,将任何输入参数直接传递给基类构造函数。

除此之外,它还声明了两个 Add 方法,第二个方法只是从其输入参数创建一个 Feedback 对象并将其传递给第一个方法。 在将新消息添加到集合之前,第一种方法首先检查反馈消息不为空或不为空,并且反馈集合中尚未包含相同的消息。

请注意,我们当前的实现使用基类 Add 方法将新项目添加到反馈集合的末尾。 我们也可以在这里使用基类中的 Insert 方法将新项目添加到集合的开头。

现在让我们看一下在内部使用这两个类的 FeedbackManager 类:

using System.ComponentModel;
using System.Runtime.CompilerServices;
using CompanyName.ApplicationName.DataModels;
using CompanyName.ApplicationName.DataModels.Collections;
namespace CompanyName.ApplicationName.Managers
{
   
    public class FeedbackManager : INotifyPropertyChanged
    {
   
        private static FeedbackCollection feedback = new FeedbackCollection();
        private static FeedbackManager instance = null;
        private FeedbackManager() {
    }
        public static FeedbackManager Instance =>
            instance ?? (instance = new FeedbackManager());
        public FeedbackCollection Feedback
        {
   
            get {
    return feedback; }
            set {
    feedback = value; NotifyPropertyChanged(); }
        }
        public void Add(Feedback feedback)
        {
   
            Feedback.Add(feedback);
        }
        public void Add(string message, bool isSuccess)
        {
   
            Add(new Feedback(message, isSuccess));
        }
        #region INotifyPropertyChanged Members
            ...
            #endregion
    }
}

FeedbackManager 类还实现了 INotifyPropertyChanged 接口,在其中我们可以看到静态的 FeedbackCollection 字段。 接下来,我们看到静态实例字段、私有构造函数和类型为 FeedbackManager 的静态 Instance 属性,它在第一次使用时实例化实例字段并告诉我们这个类遵循单例模式。

Feedback 属性紧随其后,是对 FeedbackCollection 字段的类访问。之后,我们看到了 Add 方法的许多方便的重载,使开发人员能够使用不同的参数添加反馈。 为简洁起见,我们在这里再次省略了 INotifyPropertyChanged 接口的实现,但它使用了我们之前看到的默认实现。

现在让我们关注 FeedbackControl 对象的 XAML:

<UserControl
             x:Class="CompanyName.ApplicationName.Views.Controls.FeedbackControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:Controls="clr-namespace:CompanyName.ApplicationName.Views.Controls"
             xmlns:Converters="clr-namespace:CompanyName.ApplicationName.Converters;
                               assembly=CompanyName.ApplicationName.Converters"
             xmlns:DataModels="clr-namespace:CompanyName.ApplicationName.DataModels;
                               assembly=CompanyName.ApplicationName.DataModels"
             xmlns:Panels="clr-namespace:CompanyName.ApplicationName.Views.Panels">
    <UserControl.Resources>
        <Converters:FeedbackTypeToImageSourceConverter
                                                       x:Key="FeedbackTypeToImageSourceConverter" />
        <Converters:BoolToVisibilityConverter
                                              x:Key="BoolToVisibilityConverter" />
        <ItemsPanelTemplate x:Key="AnimatedPanel">
            <Panels:AnimatedStackPanel />
        </ItemsPanelTemplate>
        <Style x:Key="SmallImageInButtonStyle" TargetType="{x:Type Image}"
               BasedOn="{StaticResource ImageInButtonStyle}">
            <Setter Property="Width" Value="16" />
            <Setter Property="Height" Value="16" />
        </Style>
        <DataTemplate x:Key="FeedbackTemplate" DataType="{x:Type
                                                         DataModels:Feedback}">
            <Grid Margin="2,1,2,0" MouseEnter="Border_MouseEnter"
                  MouseLeave="Border_MouseLeave">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="16" />
                    <ColumnDefinition />
                    <ColumnDefinition Width="24" />
                </Grid.ColumnDefinitions>
                <Image Stretch="None" Source="{Binding Type,
                                              Converter={StaticResource FeedbackTypeToImageSourceConverter}}"
                       VerticalAlignment="Top" Margin="0,4,0,0" />
                <TextBlock Grid.Column="1" Text="{Binding Message}"
                           MinHeight="22" TextWrapping="Wrap" Margin="5,2,5,0"
                           VerticalAlignment="Top" FontSize="14" />
                <Button Grid.Column="2" ToolTip="Removes this message from the
                                                 list" VerticalAlignment="Top" PreviewMouseLeftButtonDown=
                        "DeleteButton_PreviewMouseLeftButtonDown">
                    <Image Source="pack://application:,,,/
                                   CompanyName.ApplicationName;component/Images/Delete_16.png"
                           Style="{StaticResource SmallImageInButtonStyle}" />
                </Button>
            </Grid>
        </DataTemplate>
        <DropShadowEffect x:Key="Shadow" Color="Black" ShadowDepth="6"
                          Direction="270" Opacity="0.4" />
    </UserControl.Resources>
    <Border BorderBrush="{StaticResource TransparentBlack}"
            Background="White" Padding="3" BorderThickness="1,0,1,1"
            CornerRadius="0,0,5,5" Visibility="{Binding HasFeedback,
                                               Converter={StaticResource BoolToVisibilityConverter},
                                               RelativeSource={RelativeSource Mode=FindAncestor,
                                               AncestorType={x:Type Controls:FeedbackControl}}}"
            Effect="{StaticResource Shadow}">
        <ListBox MaxHeight="89" ItemsSource="{Binding Feedback,
                                             RelativeSource={RelativeSource Mode=FindAncestor,
                                             AncestorType={x:Type Controls:FeedbackControl}}}"
                 ItemTemplate="{StaticResource FeedbackTemplate}"
                 ItemsPanel="{StaticResource AnimatedPanel}"
                 ScrollViewer.HorizontalScrollBarVisibility="Disabled"
                 ScrollViewer.VerticalScrollBarVisibility="Auto" BorderThickness="0"
                 HorizontalContentAlignment="Stretch" />
    </Border>
</UserControl>

我们首先为我们的一些应用程序项目添加许多 XAML 命名空间前缀。 使用 Converters 前缀,我们将之前看到的 FeedbackTypeToImageSourceConverter 和 BoolToVisibilityConverter 类的实例添加到 UserControl.Resources 部分。 我们还重用了第 7 章“掌握实用动画”中的 AnimatedStackPanel 类。

接下来,我们看到 SmallImageInButtonStyle 样式,它基于我们之前也看到的 ImageInButtonStyle 样式,并添加了一些大小调整属性。 之后,我们看到了 FeedbackStyle 样式,它定义了每个反馈消息在我们的反馈控件中的外观。

每个反馈对象将呈现在三列中:第一列包含一个指定反馈类型的图像,使用我们之前看到的 FeedbackTypeToImageSourceConverter 类; 第二个显示 TextWrapping 值为 Wrap 的消息; 第三个包含一个带有图像的按钮,使用我们的 SmallImageInButtonStyle 样式,用户可以使用它来删除消息。

请注意,由于这纯粹是一个没有业务逻辑的 UI 控件,因此我们可以使用文件背后的代码,即使在使用 MVVM 时也是如此。 因此,我们将 MouseEnter 和 MouseLeave 事件的事件处理程序附加到包含每个反馈对象的 Grid 面板,并将 PreviewMouseLeftButtonDown 事件的另一个事件处理程序附加到删除按钮。 我们在这里拥有的最后一个资源是一个 DropShadowEffect 实例,它定义了一个小阴影效果。

对于反馈控件,我们定义了一个使用半透明边框画笔的 Border 元素,BorderThickness 值为 1,0,1,1,CornerRadius 值为 0,0,5,5。 这四个值的工作方式类似于 Margin 属性,使我们能够为四个边中的每一个设置不同的值,或者在 CornerRadius 属性的情况下为角设置不同的值。 这样,我们就可以显示一个只有三个边有边框的矩形,两个有圆角。

请注意,此边框上的 Visibility 属性由 FeedbackControl 类的 HasFeedback 属性通过 BoolToVisibilityConverter 类的实例确定。 因此,当没有要显示的反馈对象时,边框将被隐藏。 另请注意,我们的 Shadow 资源应用于边框效果属性。

在边框内,我们声明一个 ListBox 控件,其 ItemsSource 属性设置为 FeedbackControl 类的 Feedback 属性,其高度限制为最多三个反馈项,之后将显示垂直滚动条。 它的 ItemTemplate 属性设置为我们在资源部分中定义的 FeedbackTemplate。

它的 ItemsPanel 属性设置为我们声明的 AnimatedPanel 资源,用于对反馈项的入口和出口进行动画处理。 接下来,我们通过将 BorderThickness 属性设置为 0 来移除 ListBox 的默认边框,并通过将 HorizontalContentAlignment 属性设置为 Stretch 来拉伸自动生成的 ListBoxItem 对象以适合 ListBox 控件的宽度。

现在让我们看看我们的反馈控制背后的代码:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using CompanyName.ApplicationName.DataModels;
using CompanyName.ApplicationName.DataModels.Collections;
using CompanyName.ApplicationName.Extensions;
namespace CompanyName.ApplicationName.Views.Controls
{
   
    public partial class FeedbackControl : UserControl
    {
   
        private static List<DispatcherTimer> timers =
            new List<DispatcherTimer>();
        public FeedbackControl()
        {
   
            InitializeComponent();
        }
        public static readonly DependencyProperty FeedbackProperty =
            DependencyProperty.Register(nameof(Feedback),
                                        typeof(FeedbackCollection), typeof(FeedbackControl),
                                        new UIPropertyMetadata(new FeedbackCollection(),
                                                               (d, e) => ((FeedbackCollection)e.NewValue).CollectionChanged +=
                                                               ((FeedbackControl)d)
  • 10
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

0neKing2017

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值