在桌面应用开发中,标准对话框往往无法满足复杂的业务需求。最近在重构一个库存管理系统时,我遇到了需要收集多类型输入数据的场景——常规的输入框无法同时处理文本、数字、日期和下拉选择,这促使我深入研究WPF自定义输入窗口的实现方案。
采用MVVM模式构建,核心组件包括:
Window作为容器DockPanel布局管理器TextBox/ComboBox/DatePicker等输入控件ValidationRule实现数据校验xml复制<Window x:Class="CustomInput.InputDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
SizeToContent="WidthAndHeight">
<DockPanel Margin="15">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="确认" Command="{Binding ConfirmCommand}"/>
<Button Content="取消" IsCancel="True"/>
</StackPanel>
<Grid>
<!-- 动态内容区域 -->
</Grid>
</DockPanel>
</Window>
通过ContentTemplateSelector实现不同输入类型的动态渲染:
csharp复制public class InputTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if(item is TextInput) return (DataTemplate)((FrameworkElement)container).FindResource("TextTemplate");
if(item is DateInput) return (DataTemplate)((FrameworkElement)container).FindResource("DateTemplate");
return base.SelectTemplate(item, container);
}
}
创建可复用的验证规则:
csharp复制public class RequiredFieldRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo culture)
{
return string.IsNullOrWhiteSpace(value?.ToString())
? new ValidationResult(false, "该字段为必填项")
: ValidationResult.ValidResult;
}
}
在XAML中应用验证:
xml复制<TextBox>
<TextBox.Text>
<Binding Path="UserName" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:RequiredFieldRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
使用TaskCompletionSource实现模态窗口的异步调用:
csharp复制public async Task<DialogResult> ShowDialogAsync()
{
var tcs = new TaskCompletionSource<DialogResult>();
this.Closed += (s, e) => tcs.SetResult(this.DialogResult);
this.ShowDialog();
return await tcs.Task;
}
通过VisualStateManager实现交互反馈:
xml复制<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="ValidationStates">
<VisualState x:Name="Valid"/>
<VisualState x:Name="Invalid">
<Storyboard>
<ColorAnimation Storyboard.TargetName="Border"
Storyboard.TargetProperty="(Border.BorderBrush).(SolidColorBrush.Color)"
To="Red" Duration="0:0:0.2"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
使用ViewBox配合Grid实现自适应:
xml复制<Viewbox Stretch="Uniform">
<Grid Width="300" Height="200">
<!-- 内容元素 -->
</Grid>
</Viewbox>
常见问题:首次显示时焦点未正确设置
解决方案:
csharp复制protected override void OnContentRendered(EventArgs e)
{
base.OnContentRendered(e);
MoveFocus(new TraversalRequest(FocusNavigationDirection.First));
}
确保窗口显示在主显示器:
csharp复制WindowStartupLocation = WindowStartupLocation.CenterScreen;
var screen = Screen.FromPoint(new System.Drawing.Point((int)Left, (int)Top));
WindowState = WindowState.Normal;
Width = Math.Min(Width, screen.WorkingArea.Width * 0.8);
Height = Math.Min(Height, screen.WorkingArea.Height * 0.8);
对长列表使用VirtualizingStackPanel:
xml复制<ListBox VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"/>
在App.xaml中定义全局资源:
xml复制<Application.Resources>
<Style x:Key="InputFieldStyle" TargetType="Control">
<Setter Property="Margin" Value="5"/>
<Setter Property="Padding" Value="3"/>
</Style>
</Application.Resources>
集成AutoCompleteBox:
xml复制<toolkit:AutoCompleteBox
ItemsSource="{Binding Suggestions}"
FilterMode="Contains"
ValueMemberPath="DisplayName"/>
实现最近使用项缓存:
csharp复制public class InputHistoryService
{
private readonly LimitedStack<string> _history = new(10);
public void AddHistory(string input) => _history.Push(input);
public IEnumerable<string> GetHistory() => _history.AsEnumerable();
}
防止XSS攻击:
csharp复制public static string SanitizeInput(string input)
{
return System.Web.Security.AntiXss.AntiXssEncoder.HtmlEncode(input, false);
}
密码字段使用安全字符串:
csharp复制var secureString = new SecureString();
foreach (char c in password)
{
secureString.AppendChar(c);
}
使用White框架进行测试:
csharp复制[Test]
public void Should_Show_Validation_Error_When_Empty()
{
var window = WindowFactory.CreateWindow<InputDialog>();
var textBox = window.Get<TextBox>("UserNameInput");
textBox.Text = "";
Assert.IsTrue(textBox.HasValidationError);
}
验证Tab键顺序:
csharp复制Keyboard.Press(Key.Tab);
Assert.IsTrue(secondInput.IsFocused);
处理不同.NET版本差异:
xml复制<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
集成NLog记录运行时异常:
csharp复制private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
try {
// 业务代码
} catch (Exception ex) {
Logger.Error(ex, "输入窗口操作异常");
}
在实际项目中,我发现将输入验证逻辑与业务规则分离可以大幅提高组件的复用率。通过配置化的方式定义输入字段,我们成功将相同组件应用在17个不同业务场景中,开发效率提升40%以上。