乘风破浪,超清爽WPF御用MVVM框架Stylet,启动到登录设计的高阶实战
背景
接着上一篇《乘风破浪,遇见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
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步