背景
接着上一篇《乘風破浪,遇見Stylet超清爽WPF御用MVVM框架,愛不釋手的.Net Core輕量級MVVM框架》,我們已經初步認識了WPF御用的MVVM框架Stylet,基本掌握了如下內容:
- 什么是Stylet。
- 安裝Stylet模板。
- 創建Stylet示例項目。
- Stylet的單向綁定。
- Stylet的事件綁定。
- Stylet的雙向綁定。
- Stylet的對象綁定

接下來,我們一起深入使用並探索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

將其加入HelloStylet解決方案中。
dotnet sln add .\StyletLoginDesign\StyletLoginDesign.csproj

切換到它目錄。
cd .\StyletLoginDesign\
通過DotNet-Cli的Run命令來運行它。
dotnet watch run

同時我們添加下PropertyChanged.Fody包,以便幫助我們自動生成通知屬性的代碼。
dotnet add package PropertyChanged.Fody

創建啟動和登錄頁面
我們添加一組以Login登錄業務相關的頁面,分別對應三個文件:LoginView.xaml、LoginView.xaml.cs、LoginViewModel.cs。

我們添加一組以Splash登錄業務相關的頁面,分別對應三個文件:SplashView.xaml、SplashView.xaml.cs、SplashViewModel.cs。
這里直接偷個懶,可以把默認的Shell重命名為Splash即可。

使用並添加樣式字典文件
https://github.com/canton7/Stylet/wiki/Bootstrapper#adding-resource-dictionaries-to-appxaml
新手很容易會習慣性的把所有Style都寫在Window里面,這樣不利於將來的工程化,通常老手會把Style分成幾個樣式字典文件,然后做引入。
1. 新建樣式字典文件
准備一個Styles的文件夾,其實命名無所謂。
右鍵,添加,新建項,資源字典(WPF),取個文件名保存。

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>

4. 使用樣式字典文件
在對應頁面中,找到匹配類型的控件,我們可以指定它的Style為靜態資源的SplashStatusDescriptionStyle。
<TextBlock
Text="{Binding StatusDescription}"
Style="{StaticResource SplashStatusDescriptionStyle}"
/>
通過這樣改造之后,整個Xaml就很干凈了,只留下了一個靜態Style和綁定。

實現帶陰影的圓角窗體
這里有一個思路是這樣的,首先我們要把窗體弄透明,然后在窗體內部弄一個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樣式,我們設置WindowStyle為None、設置AllowsTransparency為True,並且把背景Background設置成透明Transparent。
<Window
WindowStartupLocation="CenterScreen"
Style="{StaticResource SplashWindowStyle}"
/>
然后在頁面Window將它的Style指向SplashWindowStyle,繼承前面所有的屬性,同時我們還設置WindowStartupLocation啟動位置為CenterScreen屏幕居中。
2. 構建實現圓角和陰影的一級容器
然后我們在一級Content里面放置一個Border,並且給它創建一個樣式,我們給它指定一個圓角CornerRadius,這里指向全局的圓角GlobalRoundedCornerForWindow,實際值就是8,為了突出效果,我們給它准備一張干凈的背景圖Splash_Backgroud.jpg,記得圖片要設置成內容類型,並且始終復制。
同時,還需要在Border的Effect特效屬性中給它掛載一個陰影特效DropShadowEffect。

<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,好啦,看看效果。

讓無標題欄窗體支持拖拽
就像前面我們為了視覺,我們把窗體的標題欄干掉了,嗯,這下好了,沒有了標題欄,這個窗體都無法拖動了,不要慌。
我們可以基於窗體的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();
}
好了,試試吧,這時候,你拖拽無頭窗體任何一個地方都可以了。
采用內置消息集線器
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}%)..."
}); ;
}
});
通過_eventAggregator的Publish方法可以發送指定消息體類型的消息。
看看效果,發現啟動界面的Loading百分比就動起來了。

采用內置的轉換器
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類型。
除此之外,還有另外兩個轉換器LabelledValue、DebugConverter:
在線Svg轉Png
這里拿到了微軟Logo的SVG版本,但是WPF原生只能支持PNG,那么我們用它進行轉換下。

SVG版本備用:Login_Logo.svg
打開和關閉窗體
經常我們要打開一個新窗體,這里我們要借助IWindowManager窗體管理這個接口,我們在構造函數中用IOC注入它,可以基於_windowManager的ShowWindow方法打開一個新的窗體,還可以基於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線程的一些操作,如果不這么寫,嗯,沒反應。

實現XML格式的多語言
實現WPF多語言的方式有很多種,我們來實現一種基於XML格式的多語言設計。
1. 准備多語言文件夾及不同語言XML文件
創建一個名為Languages的多語言文件夾,我們准備至少兩種語言的XML文件,分別是Languages.en-US.xml、Languages.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");
這里建議,寫好中文注釋,不然你很難去搜索,方便將來維護它。

以多語言為例的接口+實現的綁定
我們要實現一個多語言服務,通過接口+實現的方式來做。
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>
最終效果
說了這么多,先看看階段成果。


參考
- 【WPF on .NET Core 3.0】 Stylet演示項目 - 簡易圖書管理系統(1) - 登錄
- https://github.com/canton7/Stylet/wiki
- wpf圓角窗體四周陰影效果
- WPF制作圓角帶陰影窗體
- WPF窗體陰影效果以及圓角
- WPF陰影效果(DropShadowEffect)
- WPF 陰影的簡單使用(DropShadowEffect)
- DropShadowEffect 類
- WPF 多語言實現
- WPF使用X:Static做多語言支持
- WPF國際化/多語言支持
- WPF 全球化和本地化概述
- 本地化特性和注釋
- 試用新的System.Text.Json API
- https://docs.microsoft.com/zh-cn/dotnet/api/system.text.json?view=net-5.0
