目录
有时候程序在做参数界面时,需要对参数模型中的字段一一定义控件,来做数据绑定,再进行参数设置的保存。每当新增参数字段时,都需要去修改参数界面的控件。
实现按参数字段自动生成界面元素的思路是:类的反射机制,再通过特性来标定参数的类型,再返回对应的数据控件,加载到数据表格中。这样当我修改参数模型时,界面加载时也会自动的按数据模型来生成控件,就不需要再去修改界面的样式了。
目前支持的控件模式,可扩展:
cs
/// <summary>
/// 配置输入类型
/// 按类型动态添加控件
/// </summary>
public enum ConfigInputType
{
TextBox = 0,
Combox = 1,
CheckBox = 2,
IP = 3,
Int=4,
}
界面的样子:

期间使用到的一个控件包:
Nuget 搜索 Rotion 就都可以找到


1.定义参数数据模型
首先,定义数据特性和支持的控件类型:
有参数名,是否隐藏(隐藏的话就不再界面显示出来),还有就是输入类型
cs
public class ConfigDescribeAttribute : ValidationAttribute
{
private ConfigAttributeModel _model = new ConfigAttributeModel();
public ConfigDescribeAttribute(string name, bool isHidden = false, ConfigInputType inputType = ConfigInputType.TextBox)
{
_model.Name = name;
_model.IsHidden = isHidden;
_model.InputType = inputType;
}
public ConfigAttributeModel GetConfigAttribute()
{
return _model;
}
public class ConfigAttributeModel
{
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 是否隐藏
/// </summary>
public bool IsHidden { get; set; } = false;
/// <summary>
/// 数据输入类型
/// </summary>
public ConfigInputType InputType { get; set; } = ConfigInputType.TextBox;
}
}
/// <summary>
/// 配置输入类型
/// 按类型动态添加控件
/// </summary>
public enum ConfigInputType
{
TextBox = 0,
Combox = 1,
CheckBox = 2,
IP = 3,
Int=4,
}
再来定义参数对应的数据模型:
cs
/// <summary>
/// 配置文件
/// </summary>
public class P_Environment
{
/// <summary>
/// 主题色 十六进制
/// </summary>
[ConfigDescribe("主题色", true)]
public string ThemeColor { get; set; }
/// <summary>
/// 是否开机自启
/// </summary>
[ConfigDescribe("开机自启", inputType: ConfigInputType.CheckBox)]
public bool IsAutoStart { get; set; } = false;
/// <summary>
/// 延时启动
/// </summary>
[ConfigDescribe("启动延时", inputType: ConfigInputType.Int)]
public int DelayStart { get; set; } = 0;
/// <summary>
/// 产品配方
/// </summary>
[ConfigDescribe("ProductSpec", true, inputType: ConfigInputType.Combox)]
public string ProductSpec { get; set; }
/// <summary>
/// PLC IP
/// </summary>
[ConfigDescribe("PLC-IP", inputType: ConfigInputType.IP)]
public string PLCIP { get; set; } = "";
/// <summary>
/// PLC 端口
/// </summary>
[ConfigDescribe("PLC-端口", inputType: ConfigInputType.Int)]
public int PLCPort { get; set; } = 502;
/// <summary>
/// OPCAU IP
/// </summary>
[ConfigDescribe("OPCAU-IP", inputType: ConfigInputType.IP)]
public string OPCAUIP { get; set; } = "192.168.3.1";
/// <summary>
/// OPCAU 端口
/// </summary>
[ConfigDescribe("OPCAU-端口", inputType: ConfigInputType.Int)]
public int OPCAUPort { get; set; } = 4840;
/// <summary>
/// Camera IP
/// </summary>
[ConfigDescribe("扫码枪-IP", inputType: ConfigInputType.IP)]
public string CameraIP { get; set; } = "192.168.1.92";
/// <summary>
/// Camera 端口
/// </summary>
[ConfigDescribe("扫码枪-端口", inputType: ConfigInputType.Int)]
public int CameraPort { get; set; } = 2001;
/// <summary>
/// Camera Trigger
/// </summary>
[ConfigDescribe("扫码枪-触发字符")]
public string CameraTrigger { get; set; } = "Start";
/// <summary>
/// MES 系统的请求地址
/// </summary>
[ConfigDescribe("MES 系统的请求地址")]
public string MESUrl { get; set; }
/// <summary>
/// 线体编号
/// </summary>
[ConfigDescribe("线体编号")]
public string ClientCode { get; set; }
/// <summary>
/// 清理内存间隔时间 单位分
/// </summary>
[ConfigDescribe("清理内存间隔时间 单位分", inputType: ConfigInputType.Int)]
public int ClearMemoryTime { get; set; } = 1800;
/// <summary>
/// 数据库名称
/// </summary>
[ConfigDescribe("数据库名称")]
public string DB_Name { get; set; } = "pz250521c";
/// <summary>
/// 数据库连接的IP
/// </summary>
[ConfigDescribe("数据库IP", inputType: ConfigInputType.IP)]
public string DB_IP { get; set; } = "127.0.0.1";
/// <summary>
/// 数据库连接的端口
/// </summary>
[ConfigDescribe("数据库端口")]
public string DB_Port { get; set; } = "3306";
/// <summary>
/// 数据库连接的用户名
/// </summary>
[ConfigDescribe("数据库用户名")]
public string DB_User { get; set; } = "root";
/// <summary>
/// 数据库连接的用户名
/// </summary>
[ConfigDescribe("数据库密码")]
public string DB_Password { get; set; } = "123qwe";
}
还需要再定义一个 特性对应的数据模型,也就是设置界面表格对应的ItemSource,就是在读取特性数据后,保存下来进行显示的。按需自行扩展,就比如我增加了一个Combox类型的输入控件,那它需要有个数据源字段,就又增加了Combox_ItemSource,然后再赋值的时候,需要给下拉数据源添加上(下面代码会介绍)
cs
public class ConfigSettingModel
{
/// <summary>
/// 属性名称
/// </summary>
public string PropertyName { get; set; }
/// <summary>
/// 显示名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 值
/// </summary>
public object Value { get; set; }
/// <summary>
/// 数据输入类型
/// </summary>
public ConfigInputType InputType { get; set; } = ConfigInputType.TextBox;
/// <summary>
/// 下拉框类型的话,需要赋值下拉数据源
/// </summary>
public ObservableCollection<DropDownModel> Combox_ItemSource { get; set; } = new ObservableCollection<DropDownModel>();
}
2.定义按类型返回控件的转换类
数据类型定义好后,界面需要一个转换器,根据不同的输入类型,返回不同的控件类型
比如 ConfigInputType.TextBox 就显示MetroTextBox控件来显示
ConfigInputType.CheckBox 就显示 LSCheckBox
所以在扩展了ConfigInputType的时候,这个转换器也需要添加对应的返回控件的实现代码,否则默认使用文本的方式显示(MetroTextBox)
cs
using AduSkin.Controls.Metro;
using LS.WPFControlLibrary;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using WPFClient.Models.Configs;
namespace WPFClient.UCControls
{
public class InputTypeToControlConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
//if (value == null) return null;
ConfigInputType inputType = (ConfigInputType)value;
FrameworkElement control = null;
switch (inputType)
{
case ConfigInputType.Int:
case ConfigInputType.TextBox:
default:
var textBox = new MetroTextBox(); // 替换为实际MetroTextBox控件
textBox.Width = 300;
textBox.SetBinding(TextBox.TextProperty, new Binding("Value")
{
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
control = textBox;
break;
case ConfigInputType.CheckBox:
var checkBox = new LSCheckBox(); // 替换为实际LSCheckBox控件
checkBox.SetBinding(CheckBox.IsCheckedProperty, new Binding("Value")
{
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
control = checkBox;
break;
case ConfigInputType.IP:
var ipControl = new IPControl(); // 替换为实际IPControl控件
ipControl.SetBinding(IPControl.IPProperty, new Binding("Value")
{
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
control = ipControl;
break;
case ConfigInputType.Combox:
var combox = new CommonCombox(); // 替换为实际CommonCombox控件
combox.DisplayMemberPath = "Name";
combox.SelectedValuePath = "Code";
combox.Width = 300;
combox.SetBinding(ItemsControl.ItemsSourceProperty, new Binding("Combox_ItemSource"));
combox.SetBinding(Selector.SelectedValueProperty, new Binding("Value")
{
Mode = BindingMode.TwoWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
});
control = combox;
break;
}
// 统一设置控件对齐方式
if (control is Control ctrl)
{
ctrl.HorizontalAlignment = HorizontalAlignment.Left;
ctrl.VerticalAlignment = VerticalAlignment.Center;
}
return control;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
// 单向转换无需实现(控件通过绑定直接更新Value)
throw new NotImplementedException();
}
}
}
3.按数据模型来生成自定义表格列
接下来就是参数设置界面了
首先添加DataGrid作为参数数据的呈现:
先引入InputTypeConverter
然后再添加Metro:AduDataGrid 数据表格
最后再添加输入列 ,使用模板列
<DataGridTemplateColumn Width="*" Header="值">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding InputType, Converter={StaticResource InputTypeConverter}}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
XML
<Page
x:Class="WPFClient.Views.Setting.SettingPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Metro="clr-namespace:AduSkin.Controls.Metro;assembly=AduSkin"
xmlns:cfg="clr-namespace:WPFClient.Models.Configs"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:WPFClient.Views.Setting"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uc="clr-namespace:LS.WPFControlLibrary;assembly=LS.WPFControlLibrary"
xmlns:wpfUc="clr-namespace:WPFClient.UCControls"
Title="SettingPage"
d:DesignHeight="1080"
d:DesignWidth="1645"
Background="Snow"
FontSize="23"
mc:Ignorable="d">
<Page.Resources>
<wpfUc:InputTypeToControlConverter x:Key="InputTypeConverter" />
</Page.Resources>
<Grid>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="70" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel
Grid.Row="0"
Margin="10,0,0,0"
HorizontalAlignment="Left"
Orientation="Horizontal">
<uc:CommonButton Command="{Binding SaveCommand}" Content="保 存 参 数" />
</StackPanel>
<Metro:AduDataGrid
x:Name="prd_night_table"
Grid.Row="1"
AutoGenerateColumns="False"
CanUserAddRows="False"
CanUserResizeRows="False"
CanUserSortColumns="False"
EnableColumnVirtualization="True"
EnableRowVirtualization="True"
ItemsSource="{Binding ConfigList}"
ScrollViewer.CanContentScroll="True"
VirtualizingPanel.IsVirtualizing="True"
VirtualizingPanel.VirtualizationMode="Recycling">
<Metro:AduDataGrid.Columns>
<DataGridTextColumn
Width="150"
Binding="{Binding PropertyName}"
Header="属性名" />
<DataGridTextColumn
Width="300"
Binding="{Binding Name}"
Header="名称" />
<DataGridTemplateColumn Width="*" Header="值">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding InputType, Converter={StaticResource InputTypeConverter}}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</Metro:AduDataGrid.Columns>
</Metro:AduDataGrid>
</Grid>
</Grid>
</Page>
下面就是VM中的数据绑定实现:
DataGrid 的数据源为: ItemsSource="{Binding ConfigList}" =》 ConfigList
页面加载后,把特性数据和对应的值加载出来
通过反射的方式,获取特性内容和数据模型的值
下拉框的输入方式的话,需要在GetComboxItemSource根据属性名返回对应的下拉数据源
cs
public override void LoadData()
{
try
{
var cfg = GlobalData.ConfigParams;
ConfigList.Clear();
foreach (var propertyInfo in cfg.GetType().GetProperties())
{
if (propertyInfo.IsDefined(typeof(ConfigDescribeAttribute)))//如果属性上有定义该属性,此步没有构造出实例
{
var attribute = propertyInfo.GetCustomAttributes(typeof(ConfigDescribeAttribute))?.FirstOrDefault();
if (attribute != null)
{
var cfgAb = attribute as ConfigDescribeAttribute;
var model = cfgAb.GetConfigAttribute();
if (model != null && !model.IsHidden)
{
ConfigSettingModel item = new ConfigSettingModel();
item.PropertyName = propertyInfo.Name;
item.Value = propertyInfo.GetValue(cfg, null);
item.Name = model.Name;
item.InputType = model.InputType;
if (item.InputType == ConfigInputType.Combox)
{
item.Combox_ItemSource = GetComboxItemSource(item.PropertyName);
}
ConfigList.Add(item);
}
}
}
}
OnPropertyChanged(nameof(ConfigList));
}
catch (Exception ex)
{
LogOperate.Error("LoadData 发生异常", ex);
}
}
/// <summary>
/// 根据属性名返回相应的下拉框数据源
/// </summary>
/// <param name="propertyName"></param>
/// <returns></returns>
private ObservableCollection<DropDownModel> GetComboxItemSource(string propertyName)
{
ObservableCollection<DropDownModel> source = new ObservableCollection<DropDownModel>();
try
{
switch (propertyName)
{
case "ProductSpec":
foreach (var p in GlobalData.FormulaDatas)
{
source.Add(new DropDownModel()
{
//自由定义和赋值 Name对应的就是下拉显示的值 Code对应就是保存到配置文件的值
Name = p.Name,//显示文本
Code = p.ID, //实际保存的值
});
}
break;
default:
break;
}
}
catch (Exception ex)
{
LogOperate.Error("GetComboxItemSource", ex);
}
return source;
}
保存数据:
数据源ConfigList 在界面修改数据时,双向绑定后,也会更新到ConfigList对象中
所以还是根据反射的方式,将ConfigList中的数据保存到参数对象中,
pro.SetValue(cfg, prop.Value);
cs
private void Save(object obj)
{
try
{
var cfg = GlobalData.ConfigParams;
var pros = cfg.GetType().GetProperties().ToList();
foreach (var prop in ConfigList)
{
var pro = pros.Find(x => x.Name == prop.PropertyName);
if (pro != null)
{
var attribute = pro.GetCustomAttributes(typeof(ConfigDescribeAttribute))?.FirstOrDefault();
if (attribute != null)
{
var cfgAb = attribute as ConfigDescribeAttribute;
var model = cfgAb.GetConfigAttribute();
if (model != null)
{
if (model.InputType == ConfigInputType.Int)
{
pro.SetValue(cfg, Convert.ToInt32(prop.Value));
}
else
{
pro.SetValue(cfg, prop.Value);
}
}
}
else
{
try
{
pro.SetValue(cfg, prop.Value);
}
catch (Exception ex)
{
VM_MainWindow.Popup($"保存异常,{ex.Message}");
}
}
}
}
GlobalData.ConfigParams = cfg;
//开机自启操作
if (GlobalData.ConfigParams.IsAutoStart)
{
StartupManager startupManager = new StartupManager();
if (!startupManager.IsStartupEnabled())
{
startupManager.EnableStartup();
}
}
else
{
StartupManager startupManager = new StartupManager();
if (startupManager.IsStartupEnabled())
{
startupManager.DisableStartup();
}
}
var res = ConfigParamOperation.SaveConfigParam(GlobalData.ConfigParams);
if (res)
{
VM_MainWindow.Popup("保存成功");
}
else
{
VM_MainWindow.Popup($"保存失败,{res.Message}");
}
}
catch (Exception ex)
{
VM_MainWindow.Popup($"保存失败,{ex.Message}");
LogOperate.Error("SaveCommand", ex);
}
}
下面是完整的VM代码:
cs
using AduSkin.Controls.Metro;
using LS.WPF.MVVM;
using LS.WPF.MVVM.Command;
using LS.WPF.MVVM.StandardModel;
using LS.WPFControlLibrary;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using WPFClient.Models.Configs;
using WPFClient.Operation;
using WPFClient.Tools;
using WPFClient.Views.Setting;
namespace WPFClient.ViewModels.Setting
{
public class VM_SettingPage : BaseViewModel
{
public VM_SettingPage() : base(typeof(SettingPage)) { }
protected override void Page_Loaded(object sender, RoutedEventArgs e)
{
base.Page_Loaded(sender, e);
}
protected override void Page_Unloaded(object sender, RoutedEventArgs e)
{
base.Page_Unloaded(sender, e);
}
public override void LoadData()
{
try
{
var cfg = GlobalData.ConfigParams;
ConfigList.Clear();
foreach (var propertyInfo in cfg.GetType().GetProperties())
{
if (propertyInfo.IsDefined(typeof(ConfigDescribeAttribute)))//如果属性上有定义该属性,此步没有构造出实例
{
var attribute = propertyInfo.GetCustomAttributes(typeof(ConfigDescribeAttribute))?.FirstOrDefault();
if (attribute != null)
{
var cfgAb = attribute as ConfigDescribeAttribute;
var model = cfgAb.GetConfigAttribute();
if (model != null && !model.IsHidden)
{
ConfigSettingModel item = new ConfigSettingModel();
item.PropertyName = propertyInfo.Name;
item.Value = propertyInfo.GetValue(cfg, null);
item.Name = model.Name;
item.InputType = model.InputType;
if (item.InputType == ConfigInputType.Combox)
{
item.Combox_ItemSource = GetComboxItemSource(item.PropertyName);
}
ConfigList.Add(item);
}
}
}
}
OnPropertyChanged(nameof(ConfigList));
}
catch (Exception ex)
{
LogOperate.Error("LoadData 发生异常", ex);
}
}
/// <summary>
/// 根据属性名返回相应的下拉框数据源
/// </summary>
/// <param name="propertyName"></param>
/// <returns></returns>
private ObservableCollection<DropDownModel> GetComboxItemSource(string propertyName)
{
ObservableCollection<DropDownModel> source = new ObservableCollection<DropDownModel>();
try
{
switch (propertyName)
{
case "ProductSpec":
foreach (var p in GlobalData.FormulaDatas)
{
source.Add(new DropDownModel()
{
//自由定义和赋值 Name对应的就是下拉显示的值 Code对应就是保存到配置文件的值
Name = p.Name,//显示文本
Code = p.ID, //实际保存的值
});
}
break;
default:
break;
}
}
catch (Exception ex)
{
LogOperate.Error("GetComboxItemSource", ex);
}
return source;
}
public DelegateCommand SaveCommand
{
get { return new DelegateCommand(Save); }
}
private void Save(object obj)
{
try
{
var cfg = GlobalData.ConfigParams;
var pros = cfg.GetType().GetProperties().ToList();
foreach (var prop in ConfigList)
{
var pro = pros.Find(x => x.Name == prop.PropertyName);
if (pro != null)
{
var attribute = pro.GetCustomAttributes(typeof(ConfigDescribeAttribute))?.FirstOrDefault();
if (attribute != null)
{
var cfgAb = attribute as ConfigDescribeAttribute;
var model = cfgAb.GetConfigAttribute();
if (model != null)
{
if (model.InputType == ConfigInputType.Int)
{
pro.SetValue(cfg, Convert.ToInt32(prop.Value));
}
else
{
pro.SetValue(cfg, prop.Value);
}
}
}
else
{
try
{
pro.SetValue(cfg, prop.Value);
}
catch (Exception ex)
{
VM_MainWindow.Popup($"保存异常,{ex.Message}");
}
}
}
}
GlobalData.ConfigParams = cfg;
//开机自启操作
if (GlobalData.ConfigParams.IsAutoStart)
{
StartupManager startupManager = new StartupManager();
if (!startupManager.IsStartupEnabled())
{
startupManager.EnableStartup();
}
}
else
{
StartupManager startupManager = new StartupManager();
if (startupManager.IsStartupEnabled())
{
startupManager.DisableStartup();
}
}
var res = ConfigParamOperation.SaveConfigParam(GlobalData.ConfigParams);
if (res)
{
VM_MainWindow.Popup("保存成功");
}
else
{
VM_MainWindow.Popup($"保存失败,{res.Message}");
}
}
catch (Exception ex)
{
VM_MainWindow.Popup($"保存失败,{ex.Message}");
LogOperate.Error("SaveCommand", ex);
}
}
private ObservableCollection<ConfigSettingModel> _cfgList = new ObservableCollection<ConfigSettingModel>();
/// <summary>
/// 配置数据集
/// </summary>
public ObservableCollection<ConfigSettingModel> ConfigList
{
get { return _cfgList; }
set { _cfgList = value; OnPropertyChanged(); }
}
}
}