乘风破浪,超清爽WPF御用MVVM框架Stylet,启动到登录设计的高阶实战

背景

接着上一篇《乘风破浪,遇见Stylet超清爽WPF御用MVVM框架,爱不释手的.Net Core轻量级MVVM框架》,我们已经初步认识了WPF御用的MVVM框架Stylet,基本掌握了如下内容:

  • 什么是Stylet。
  • 安装Stylet模板。
  • 创建Stylet示例项目。
  • Stylet的单向绑定。
  • Stylet的事件绑定。
  • Stylet的双向绑定。
  • Stylet的对象绑定

image

接下来,我们一起深入使用并探索Stylet更多高阶使用技巧,其中包括:

  • 创建多页面示例项目
  • 使用并添加样式字典文件
  • 实现带阴影的圆角窗体
  • 让无标题栏窗体支持拖拽
  • 采用内置消息集线器
  • 采用内置的转换器
  • 在线Svg转Png
  • 打开和关闭窗体
  • 跨UI线程调用
  • 实现XML格式的多语言
  • 以多语言为例的接口+实现的绑定
  • 试试内置的System.Text.Json
  • 实现WPF密码输入框的绑定

Stylet高阶实战

https://github.com/TaylorShi/HelloStylet/tree/master/StyletLoginDesign

创建名为StyletLoginDesign的示例项目

通过Dotnet-Cli创建一个基于stylet模板,名为StyletLoginDesign的项目。

dotnet new stylet -o StyletLoginDesign

image

将其加入HelloStylet解决方案中。

dotnet sln add .\StyletLoginDesign\StyletLoginDesign.csproj

image

切换到它目录。

cd .\StyletLoginDesign\

通过DotNet-CliRun命令来运行它。

dotnet watch run

image

同时我们添加下PropertyChanged.Fody包,以便帮助我们自动生成通知属性的代码。

dotnet add package PropertyChanged.Fody

image

创建启动和登录页面

我们添加一组以Login登录业务相关的页面,分别对应三个文件:LoginView.xamlLoginView.xaml.csLoginViewModel.cs

image

我们添加一组以Splash登录业务相关的页面,分别对应三个文件:SplashView.xamlSplashView.xaml.csSplashViewModel.cs

这里直接偷个懒,可以把默认的Shell重命名为Splash即可。

image

使用并添加样式字典文件

https://github.com/canton7/Stylet/wiki/Bootstrapper#adding-resource-dictionaries-to-appxaml

新手很容易会习惯性的把所有Style都写在Window里面,这样不利于将来的工程化,通常老手会把Style分成几个样式字典文件,然后做引入。

1. 新建样式字典文件

准备一个Styles的文件夹,其实命名无所谓。

右键,添加,新建项,资源字典(WPF),取个文件名保存。

image

2. 引用样式字典文件

有了字典文件,下一步就是引入它。

双击打开App.xaml文件,编辑它,插入ApplicationLoader.MergedDictionaries字典组的ResourceDictionary节点。

<Application x:Class="StyletLoginDesign.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:s="https://github.com/canton7/Stylet"
             xmlns:local="clr-namespace:StyletLoginDesign">
    <Application.Resources>
        <s:ApplicationLoader>
            <s:ApplicationLoader.Bootstrapper>
                <local:Bootstrapper/>
            </s:ApplicationLoader.Bootstrapper>

            <s:ApplicationLoader.MergedDictionaries>
                <ResourceDictionary Source="./Styles/GlobalStyle.xaml"/>
                <ResourceDictionary Source="./Styles/SplashStyle.xaml"/>
                <ResourceDictionary Source="./Styles/LoginStyle.xaml"/>
            </s:ApplicationLoader.MergedDictionaries>
        </s:ApplicationLoader>
    </Application.Resources>
</Application>

3. 定义样式字典文件

完成前面两步,我们试着添加一个针对TextBlock控件类型的SplashStatusDescriptionStyle样式字典Key。

<!-- 启动界面 状态描述 -->
<Style x:Key="SplashStatusDescriptionStyle" TargetType="{x:Type TextBlock}">
    <Setter Property="Foreground" Value="#FFFFFFFF" />
    <Setter Property="FontSize" Value="16" />
    <Setter Property="HorizontalAlignment" Value="Center" />
    <Setter Property="VerticalAlignment" Value="Bottom" />
    <Setter Property="Margin" Value="0,0,0,24" />
</Style>

image

4. 使用样式字典文件

在对应页面中,找到匹配类型的控件,我们可以指定它的Style为静态资源的SplashStatusDescriptionStyle

<TextBlock
    Text="{Binding StatusDescription}"
    Style="{StaticResource SplashStatusDescriptionStyle}"
    />

通过这样改造之后,整个Xaml就很干净了,只留下了一个静态Style和绑定。

image

实现带阴影的圆角窗体

这里有一个思路是这样的,首先我们要把窗体弄透明,然后在窗体内部弄一个Border,我们通过它实现圆角,同时基于它做一个DropShadowEffect的阴影效果,接下来我们动手试试:

1. 构建支持圆角的透明窗体样式

<Style x:Key="SplashWindowStyle" TargetType="{x:Type Window}">
    <Setter Property="Width" Value="700" />
    <Setter Property="Height" Value="400" />
    <Setter Property="ResizeMode" Value="NoResize" />
    <Setter Property="WindowStyle" Value="None" />
    <Setter Property="AllowsTransparency" Value="True" />
    <Setter Property="Background" Value="Transparent" />
</Style>

这里我们设计一个针对Window类型的SplashWindowStyle样式,我们设置WindowStyleNone、设置AllowsTransparencyTrue,并且把背景Background设置成透明Transparent

<Window
    WindowStartupLocation="CenterScreen"
    Style="{StaticResource SplashWindowStyle}"
    />

然后在页面Window将它的Style指向SplashWindowStyle,继承前面所有的属性,同时我们还设置WindowStartupLocation启动位置为CenterScreen屏幕居中。

2. 构建实现圆角和阴影的一级容器

然后我们在一级Content里面放置一个Border,并且给它创建一个样式,我们给它指定一个圆角CornerRadius,这里指向全局的圆角GlobalRoundedCornerForWindow,实际值就是8,为了突出效果,我们给它准备一张干净的背景图Splash_Backgroud.jpg,记得图片要设置成内容类型,并且始终复制

同时,还需要在BorderEffect特效属性中给它挂载一个阴影特效DropShadowEffect

image

<Style x:Key="SplashBorderStyle" TargetType="{x:Type Border}">
    <Setter Property="CornerRadius" Value="{StaticResource GlobalRoundedCornerForWindow}" />
    <Setter Property="Margin" Value="8" />
    <Setter Property="Background">
        <Setter.Value>
            <ImageBrush ImageSource="../Assets/Splash/Splash_Backgroud.jpg"/>
        </Setter.Value>
    </Setter>
    <Setter Property="Effect">
        <Setter.Value>
            <DropShadowEffect ShadowDepth="0" BlurRadius="12"/>
        </Setter.Value>
    </Setter>
</Style>

然后回到Window中,给它挂载这个样式。

<Border Style="{StaticResource SplashBorderStyle}">
    <TextBlock
        Text="{Binding StatusDescription}"
        Style="{StaticResource SplashStatusDescriptionStyle}"
        />
</Border>

3. 运行看看效果

Border中间,我们给他弄个文本,显示当前状态描述StatusDescription,好啦,看看效果。

image

让无标题栏窗体支持拖拽

就像前面我们为了视觉,我们把窗体的标题栏干掉了,嗯,这下好了,没有了标题栏,这个窗体都无法拖动了,不要慌。

我们可以基于窗体的MouseLeftButtonDown事件来完成这个动作,很简单。

在Window界面上,我们绑定它的MouseLeftButtonDown事件到Window_MouseLeftButtonDown方法。

<Window 
    xmlns:s="https://github.com/canton7/Stylet"
    MouseLeftButtonDown="{s:Action Window_MouseLeftButtonDown}"
    >
</Window>

我们看看在VM里面,这个响应方法的定义。

/// <summary>
/// 响应鼠标左键按下的事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void Window_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    // 让窗体随着拖拽移动
    ((System.Windows.Window)sender).DragMove();
}

好了,试试吧,这时候,你拖拽无头窗体任何一个地方都可以了。

采用内置消息集线器

https://github.com/canton7/Stylet/wiki/The-EventAggregator

Stylet内置了消息集线器EventAggregator,使用它主要是就是三个步骤,通过它,我们可以在页面之间传递消息,也可以自己订阅自己发送。

这里的案例是,我需要在启动页面做一些耗时操作,并且希望实时把进度给同步回界面进行更新,那么我们在界面这里直接订阅消息,在其他任何地方进行发送就行。

1. 定义消息体。

/// <summary>
/// 更新启动状态描述
/// </summary>
public class UpdateSplashStatusDescriptionEvent
{
    /// <summary>
    /// 状态描述
    /// </summary>
    /// <value></value>
    public string StatusDescription { get; set; }
}

2. 继承消息接口

这里直接在页面继承IHandle<UpdateSplashStatusDescriptionEvent>这个接口,它会要求你实现一个Handle(UpdateSplashStatusDescriptionEvent message)用来接收,如果有多个消息,可以继承多个接口,写多个实现就是了。

/// <summary>
/// 启动界面
/// </summary>
public class SplashViewModel : Screen, IHandle<UpdateSplashStatusDescriptionEvent>
{
    /// <summary>
    /// 接收来自更新启动状态描述的消息
    /// </summary>
    /// <param name="message"></param>
    public void Handle(UpdateSplashStatusDescriptionEvent message)
    {
        StatusDescription = message.StatusDescription;
    }
}

3. 订阅消息

/// <summary>
/// 启动界面
/// </summary>
public class SplashViewModel : Screen, IHandle<UpdateSplashStatusDescriptionEvent>
{
    /// <summary>
    /// 事件集线器
    /// </summary>
    private readonly IEventAggregator _eventAggregator;

    /// <summary>
    /// 构造函数
    /// </summary>
    public SplashViewModel(IEventAggregator eventAggregator)
    {
        _eventAggregator = eventAggregator;
    }

    protected override void OnViewLoaded()
    {
        // 订阅消息
        _eventAggregator.Subscribe(this);
    }
}

通过IOC引入IEventAggregator得到_eventAggregator,然后通过Subscribe(this)进行订阅。

4. 发送消息

// 异步线程通知更新
Task.Factory.StartNew(async () => {
    for (var i = 0; i <= 99; i++)
    {
        await Task.Delay(TimeSpan.FromMilliseconds(12.5));

        // 发送消息
        _eventAggregator.Publish(new UpdateSplashStatusDescriptionEvent
        {
            StatusDescription = $"{Splash_StatusDescription}({i + 1}%)..."
        }); ;
    }
});

通过_eventAggregatorPublish方法可以发送指定消息体类型的消息。

看看效果,发现启动界面的Loading百分比就动起来了。

image

采用内置的转换器

Stylet内置了一些常用的转换器,比如我们经常需要基于一个Boolean类型转成界面的显示和隐藏,这时候我们需要使用到BoolToVisibilityConverter

https://github.com/canton7/Stylet/wiki/BoolToVisibilityConverter

使用起来很简单,在我们的全局样式字典GlobalStyle.xaml中添加它。

<!-- 全局转换器 布尔值转可视化状态 -->
<s:BoolToVisibilityConverter
    x:Key="BoolToVisConverter"
    TrueVisibility="Visible"
    FalseVisibility="Hidden"
    />

在页面的Xaml中使用它。

<Button Visibility="{Binding IsOpenRegister,Converter={StaticResource BoolToVisConverter}}"/>

这里绑定的是一个叫IsOpenRegister的布尔值,它会自动把布尔值转成我们要的Visibility类型。

除此之外,还有另外两个转换器LabelledValueDebugConverter

在线Svg转Png

https://svgtopng.com

这里拿到了微软Logo的SVG版本,但是WPF原生只能支持PNG,那么我们用它进行转换下。

image

SVG版本备用:Login_Logo.svg

打开和关闭窗体

https://github.com/canton7/Stylet/wiki/The-WindowManager

经常我们要打开一个新窗体,这里我们要借助IWindowManager窗体管理这个接口,我们在构造函数中用IOC注入它,可以基于_windowManagerShowWindow方法打开一个新的窗体,还可以基于ShowDialog方法弹出一个新窗体,最后我们可以通过RequestClose这个方法请求当前窗体进行关闭。

/// <summary>
/// 启动界面
/// </summary>
public class SplashViewModel : Screen
{
    /// <summary>
    /// 窗口管理
    /// </summary>
    private IWindowManager _windowManager;

    /// <summary>
    /// Ioc容器
    /// </summary>
    private IContainer _container;

    /// <summary>
    /// 构造函数
    /// </summary>
    /// <param name="windowManager"></param>
    public SplashViewModel(IWindowManager windowManager, IContainer container)
    {
        _windowManager = windowManager;
        _container = container;
    }

    /// <summary>
    /// 开始状态更新
    /// </summary>
    private void StartStatusUpdate()
    {
        ...

        Execute.OnUIThread(()=> {

            var loginViewModel = _container.Get<LoginViewModel>();
            _windowManager.ShowWindow(loginViewModel);
            RequestClose();
        });
    }
}

这里留意到,我们用到一个IContainer的容器接口,通过它的Get方法,我们可以拿到LoginViewModel的页面实例。

跨UI线程调用

https://github.com/canton7/Stylet/wiki/Execute%3A-Dispatching-to-the-UI-thread

这其实在桌面编程里面是很常见的一个需求,就是比如你离开了UI线程去做了一些事情,回头来,你又要操作UI线程进行界面更新,这时候你发现直接这么写是不行的,因为你无法从子系统来操作UI线程。

实际上传统的方法,我们经常用Application.Current.Dispatcher.BeginInvoke来做。

但是在Stylet中其实内置了对应的方法来支持。

/// <summary>
/// 开始状态更新
/// </summary>
private void StartStatusUpdate()
{
    var Splash_StatusDescription = _langService.GetXmlLocalizedString("Splash_StatusDescription");

    // 异步线程通知更新
    Task.Factory.StartNew(async () => {
        for (var i = 0; i <= 99; i++)
        {
            await Task.Delay(TimeSpan.FromMilliseconds(12.5));

            // 发送消息
            _eventAggregator.Publish(new UpdateSplashStatusDescriptionEvent
            {
                StatusDescription = $"{Splash_StatusDescription}({i + 1}%)..."
            }); ;
        }

        Execute.OnUIThread(()=> {

            var loginViewModel = _container.Get<LoginViewModel>();
            _windowManager.ShowWindow(loginViewModel);
            RequestClose();
        });
    });
}

从上面这个函数我们可以看到,我们在一个Task里面完成了一些事情,然后通过Execute.OnUIThread这个方法执行关于UI线程的一些操作,如果不这么写,嗯,没反应。

image

实现XML格式的多语言

实现WPF多语言的方式有很多种,我们来实现一种基于XML格式的多语言设计。

1. 准备多语言文件夹及不同语言XML文件

创建一个名为Languages的多语言文件夹,我们准备至少两种语言的XML文件,分别是Languages.en-US.xmlLanguages.zh-CN.xml,它的默认内容是:

<?xml version="1.0" encoding="utf-8"?>
<language>
  <resources>
  </resources>
</language>

这里设计了一个language根节点和resources子节点。

2. 填充多语言的语言Key

<!-- Splash -->
<Splash_Title>启动</Splash_Title>
<Splash_StatusDescription>启动中</Splash_StatusDescription>
<!-- Splash -->
<Splash_Title>Splash</Splash_Title>
<Splash_StatusDescription>Loading</Splash_StatusDescription>

建议书写时,按组来,并且写好注释。

3. 挂载多语言Xml多语言

编辑App.xaml文件,在原来的s:ApplicationLoader.MergedDictionaries中添加一个新的ResourceDictionary,它的类型是XmlDataProvider,我们给它一个命名叫Lang

<Application x:Class="StyletLoginDesign.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:s="https://github.com/canton7/Stylet"
             xmlns:local="clr-namespace:StyletLoginDesign">
    <Application.Resources>
        <s:ApplicationLoader>
            <s:ApplicationLoader.Bootstrapper>
                <local:Bootstrapper/>
            </s:ApplicationLoader.Bootstrapper>

            <s:ApplicationLoader.MergedDictionaries>
                <ResourceDictionary>
                    <XmlDataProvider x:Key="Lang" Source="Languages/Languages.zh-CN.xml" XPath="language/resources" IsAsynchronous="False"/>
                </ResourceDictionary>
            </s:ApplicationLoader.MergedDictionaries>
        </s:ApplicationLoader>
    </Application.Resources>
</Application>

4. 界面上使用多语言

在Xaml中使用多语言非常简单,只需要指定XPath就行。

<Button Content="{Binding Source={StaticResource Lang},XPath=Splash_Title}" />

5. 加载XML多语言到内存中

为了在VM里面使用多语言,我们需要先能把XML文件加载到一个内存实例中,我们这里准备了一个LanguageContextService

/// <summary>
/// LanguageContextService
/// </summary>
public class LanguageContextService : Singleton<LanguageContextService>
{
    /// <summary>
    /// 多语言对象
    /// </summary>
    public XmlDataProvider Provider { get; set; }
}

这里用到一个扩展类Singleton,用来控制对象单例。

public class Singleton<T> where T : class, new()
{
    private static T _instance;
    private static readonly object SysLock = new object();

    public static T Instance()
    {
        if (_instance == null)
        {
            lock (SysLock)
            {
                if (_instance == null)
                {
                    _instance = new T();
                }
            }
        }
        return _instance;
    }
}

然后我们在主界面进来的时候,把XML加载到LanguageContextService实例上来。

/// <summary>
/// 窗体加载完毕
/// </summary>
protected override void OnViewLoaded()
{
    LanguageContextService.Instance().Provider = System.Windows.Application.Current.TryFindResource("Lang") as XmlDataProvider;
}

这里直接让它去找Lang这个字典就行了。

有了LanguageContextService实例,后续如果有切换功能我们可以切换加载:

/// <summary>
/// 窗体加载完毕
/// </summary>
protected override void OnViewLoaded()
{
    var lanSourcePath = $"Languages/Languages.{"en-US"}.xml";
    var lanUri = new Uri(lanSourcePath, UriKind.Relative);
    LanguageContextService.Instance().Provider.Source = lanUri;
    LanguageContextService.Instance().Provider.Refresh();
}

6. 获取对应Key的多语言

然后就是获取LanguageContextService实例中对应的多语言了,可以通过Key来获取就是了,也就是之前的XPath的值。

// 启动中
var Splash_StatusDescription = _langService.GetXmlLocalizedString("Splash_StatusDescription");

这里建议,写好中文注释,不然你很难去搜索,方便将来维护它。

image

以多语言为例的接口+实现的绑定

https://github.com/canton7/Stylet/wiki/Bootstrapper

我们要实现一个多语言服务,通过接口+实现的方式来做。

1. 定义好接口和实现。

public class LangService : ILangService
{
    private ILogService _logService;

    public LangService(ILogService logService)
    {
        _logService = logService;
    }

    /// <summary>
    /// 丢失多语言上下文文档
    /// </summary>
    private readonly string MissLanguageContextDocument = "丢失多语言上下文文档";

    /// <summary>
    /// 未找到多语言文档Key
    /// </summary>
    private readonly string MissLanguageContextKey = "未找到多语言文档Key";

    /// <summary>
    /// GetXmlLocalizedString
    /// </summary>
    /// <param name="key"></param>
    /// <param name="defaultMessage"></param>
    /// <returns></returns>
    public string GetXmlLocalizedString(string key, string defaultMessage = "")
    {
        if (string.IsNullOrEmpty(key))
            return defaultMessage;

        var langContent = defaultMessage;
        try
        {
            var langDocument = LanguageContextService.Instance().Provider?.Document;
            if (langDocument != null)
            {
                var langKeyPath = $"/language/resources/{key}";
                var langKeyNode = langDocument?.SelectSingleNode(langKeyPath);
                if (langKeyNode != null)
                {
                    langContent = langKeyNode?.InnerText;
                }
                else
                {
                    _logService.LogError(null, GetType(), MissLanguageContextKey, Guid.NewGuid().ToString());
                }
            }
            else
            {
                _logService.LogError(null, GetType(), MissLanguageContextDocument, Guid.NewGuid().ToString());
            }
        }
        catch (Exception ex)
        {
            _logService.LogError(ex, GetType(), MissLanguageContextKey, Guid.NewGuid().ToString());
        }
        return langContent;
    }
}

public interface ILangService
{
    /// <summary>
    /// GetXmlLocalizedString
    /// </summary>
    /// <param name="key"></param>
    /// <param name="defaultMessage"></param>
    /// <returns></returns>
    string GetXmlLocalizedString(string key, string defaultMessage = "");
}

2. 绑定接口和实现

在Stylet中,因为采用的是IOC的方式进行注入,那么我们前往Bootstrapper.cs文件的ConfigureIoC方法,添加指定接口和实现之间的绑定关系。

public class Bootstrapper : Bootstrapper<SplashViewModel>
{
    protected override void ConfigureIoC(IStyletIoCBuilder builder)
    {
        // Configure the IoC container in here
        builder.Bind<ILangService>().To<LangService>();
    }
}

3. 使用IOC注入

在需要使用接口方法的地方,我们通过构造函数IOC注入即可得到对应的实现实例。

/// <summary>
/// 启动界面
/// </summary>
public class SplashViewModel : Screen
{
    /// <summary>
    /// 语言服务
    /// </summary>
    private readonly ILangService _langService;

    /// <summary>
    /// 构造函数
    /// </summary>
    public SplashViewModel(ILangService langService)
    {
        _langService = langService;
    }
}

然后便可使用ILangService接口中的方法了。

var Splash_StatusDescription = _langService.GetXmlLocalizedString("Splash_StatusDescription");

试试内置的System.Text.Json

https://docs.microsoft.com/zh-cn/dotnet/api/system.text.json?view=net-5.0

/// <summary>
/// 记录入参日志
/// </summary>
/// <param name="description"></param>
/// <param name="typePoint"></param>
/// <param name="vo"></param>
/// <param name="requestId"></param>
public void LogVo(string description, Type typePoint, Object vo, string requestId)
{
    var contentStr = vo != null ? JsonSerializer.Serialize(vo) : string.Empty;
    _logger.Info($"{description}, 入参:requestId {requestId} functionName:{ typePoint } content: {contentStr}");
}

实现WPF密码输入框的绑定

这里借助一个PasswordBoxHelper来做。

/// <summary>
/// Password 绑定功能
/// </summary>
public static class PasswordBoxHelper
{
    public static readonly DependencyProperty PasswordProperty =
        DependencyProperty.RegisterAttached("Password",
            typeof(string), typeof(PasswordBoxHelper),
            new FrameworkPropertyMetadata(string.Empty, OnPasswordPropertyChanged));
    public static readonly DependencyProperty AttachProperty =
        DependencyProperty.RegisterAttached("Attach",
            typeof(bool), typeof(PasswordBoxHelper), new PropertyMetadata(false, Attach));
    private static readonly DependencyProperty IsUpdatingProperty =
        DependencyProperty.RegisterAttached("IsUpdating", typeof(bool),
            typeof(PasswordBoxHelper));

    public static void SetAttach(DependencyObject dp, bool value)
    {
        dp.SetValue(AttachProperty, value);
    }
    public static bool GetAttach(DependencyObject dp)
    {
        return (bool)dp.GetValue(AttachProperty);
    }
    public static string GetPassword(DependencyObject dp)
    {
        return (string)dp.GetValue(PasswordProperty);
    }
    public static void SetPassword(DependencyObject dp, string value)
    {
        dp.SetValue(PasswordProperty, value);
    }
    private static bool GetIsUpdating(DependencyObject dp)
    {
        return (bool)dp.GetValue(IsUpdatingProperty);
    }
    private static void SetIsUpdating(DependencyObject dp, bool value)
    {
        dp.SetValue(IsUpdatingProperty, value);
    }
    private static void OnPasswordPropertyChanged(DependencyObject sender,
        DependencyPropertyChangedEventArgs e)
    {
        PasswordBox passwordBox = sender as PasswordBox;
        passwordBox.PasswordChanged -= PasswordChanged;
        if (!(bool)GetIsUpdating(passwordBox))
        {
            passwordBox.Password = (string)e.NewValue;
        }
        passwordBox.PasswordChanged += PasswordChanged;
    }
    private static void Attach(DependencyObject sender,
        DependencyPropertyChangedEventArgs e)
    {
        PasswordBox passwordBox = sender as PasswordBox;
        if (passwordBox == null)
            return;
        if ((bool)e.OldValue)
        {
            passwordBox.PasswordChanged -= PasswordChanged;
        }
        if ((bool)e.NewValue)
        {
            passwordBox.PasswordChanged += PasswordChanged;
        }
    }
    private static void PasswordChanged(object sender, RoutedEventArgs e)
    {
        PasswordBox passwordBox = sender as PasswordBox;
        SetIsUpdating(passwordBox, true);
        SetPassword(passwordBox, passwordBox.Password);
        SetIsUpdating(passwordBox, false);
    }
}

然后在Xaml界面上使用它。

<PasswordBox
    Grid.Column="1"
    Style="{StaticResource LoginPasswordInputTextBox}"
    x:Name="Password"
    helper:PasswordBoxHelper.Attach="True"
    helper:PasswordBoxHelper.Password="{Binding Password,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
    PasswordChar="*"
    IsEnabled="{Binding PasswordIsEnabled}"
    >
</PasswordBox>

最终效果

说了这么多,先看看阶段成果。

image

image

参考

posted @ 2021-07-25 01:54  TaylorShi  阅读(3286)  评论(9编辑  收藏  举报