乘風破浪,超清爽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

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM