目标
做一个类似 vs2019 属性的 PropertyGrid,支持对各种属性的编辑,省去为各种类型制作编辑表单的工作。
PropertyGrid的主要特性:
- 支持 bool, int, double, string, object, 集合 属性的编辑;
- int double 类型可设定最大值、最小值、默认值,可以给定单位;
- string 类型可指定编辑器为颜色;
- string类型可从下来列表框中选择;
- object, 集合 按照父子行显示,父行只读;
- 可以设定属性不可编辑。
Ant Blazor Table
本案主要利用了 在Ant Design Blazor 官方文档中的Table的动态数据和可展开两个特性。
方案构成
- Property 为各种类型 (bool, int, double, string, object, 集合) 定义的属性元数据;
- TypeDef 对应一种被编辑对象,包含所有可编辑Property;
- Editor 用来编辑TypeDef 的工具,结果保存为json格式;
- PropertyGrid,主角,根据TypeDef构造一个Table,每个Property占一行,遇到object或集合 Property时构造父子行。
PropertyGrid.razor 主要逻辑
_Datas由TypeDef 中的Property集合构造,每个Property转换为一个Data,每个Data对应表的一行,行的编辑控件取决于Property的类型。
@using System.ComponentModel
<Table TItem="Data"
DataSource="_Datas"
TreeChildren="item=> item.Children"
Size=@TableSize.Small
PageSize="100"
Bordered
HidePagination>
<ChildContent>
@if (context.Visible)
{
<Column TData="string">
@context.Description
</Column>
<Column TData="string">
@if (context.Editable)
{
if (context.DataType == Meta.Properties.DataType.Boolean)
{
<Checkbox @bind-Value="context.BoolValue" />
}
else if (context.DataType == Meta.Properties.DataType.Int32)
{
<AntDesign.InputNumber @bind-Value="context.IntValue" Min="context.MinInt" Max="context.MaxInt"/>
}
else if (context.DataType == Meta.Properties.DataType.Double)
{
<AntDesign.InputNumber @bind-Value="context.DoubleValue" Min="context.MinDouble" Max="context.MaxDouble"/>
}
else if (context.DataType == Meta.Properties.DataType.String)
{
if (context.Editor == "color")
{
<InputGroup Compact>
<Input Type="color" @bind-Value="context.StringValue" style="padding:0; max-width:3rem"/>
<Input @bind-Value="context.StringValue" spellcheck="false"/>
</InputGroup>
}
else if (context.Options == null || context.Options.Count == 0)
{
<Input @bind-Value="context.StringValue" />
}
else
{
<Select @bind-Value="context.StringValue"
DefaultValue=context.StringValue
TItemValue="string"
TItem="string">
<SelectOptions>
@foreach (var item in context.Options)
{
<SelectOption TItemValue="string" TItem="string" Value=@item Label=@item />
}
</SelectOptions>
</Select>
}
}
}
else
{
@context.Value
}
</Column>
<Column TData="string">
@context.Unit
</Column>
}
</ChildContent>
</Table>
Data类
Data主要目的是合并各类property的属性,以便简化PropertyGrid.razor的绑定。
public class Data
{
IProperty _Property;
public Action<IProperty,object> ValueChanged;
public Data(IProperty property)
{
_Property = property;
}
public string Name { get { return _Property.Name; } }
public DataType DataType { get { return _Property.DataType; } }
public string Description { get { return _Property?.Description; } set { _Property.Description = value; } }
public string Category { get { return _Property.Category; } }
public bool Visible { get { return _Property!=null?_Property.Visible:true; } }
public bool Editable { get { return _Property != null ? _Property.Editable : false; } }
public string Editor
{
get
{
if (_Property != null && _Property is StringProperty sp)
{
return sp.Editor;
}
return string.Empty;
}
}
public bool BoolValue
{
get
{
if(_Property != null && _Property.Value != null && _Property is BoolProperty)
{
return (bool)_Property.Value;
}
return false;
}
set
{
_Property.Value = value;
if (ValueChanged != null)
{
ValueChanged.Invoke(_Property, value);
}
}
}
public int IntValue
{
get
{
if (_Property != null && _Property.Value != null && _Property is IntegerProperty)
{
return (int)_Property.Value;
}
return 0;
}
set
{
_Property.Value = value;
if (ValueChanged != null)
{
ValueChanged.Invoke(_Property, value);
}
}
}
public double DoubleValue
{
get
{
if (_Property != null && _Property.Value != null && _Property is DoubleProperty)
{
return (double)_Property.Value;
}
return 0;
}
set
{
_Property.Value = value;
if (ValueChanged != null)
{
ValueChanged.Invoke(_Property, value);
}
}
}
public string Formatter(double value)
{
int precise = 2;
if (_Property != null && _Property is DoubleProperty dp)
{
precise = dp.Precise;
}
var str = value.ToString();
var dotindex = str.IndexOf('.');
if (str.Length > dotindex + precise + 1)
{
return value.ToString($"f{precise}");
}
return str;
}
public string StringValue
{
get
{
if (_Property != null && _Property.Value != null && _Property is StringProperty)
{
return (string)_Property.Value;
}
return string.Empty;
}
set
{
_Property.Value = value;
if (ValueChanged != null)
{
ValueChanged.Invoke(_Property, value);
}
}
}
public string Value
{
get
{
if (_Property != null && _Property.Value != null && _Property is DoubleProperty dp)
{
return Formatter((double)dp.Value);
}
return _Property?.StringValue;
}
}
public int MinInt
{
get
{
if (_Property != null && _Property is IntegerProperty ip && ip.MinValue != null) return ip.MinValue.Value;
return int.MinValue;
}
}
public int MaxInt
{
get
{
if (_Property != null && _Property is IntegerProperty ip && ip.MaxValue != null) return ip.MaxValue.Value;
return int.MaxValue;
}
}
public double MinDouble
{
get
{
if (_Property != null && _Property is DoubleProperty dp && dp.MinValue != null) return dp.MinValue.Value;
return double.MinValue;
}
}
public double MaxDouble
{
get
{
if (_Property != null && _Property is DoubleProperty dp && dp.MaxValue != null) return dp.MaxValue.Value;
return double.MaxValue;
}
}
public string Unit
{
get
{
if (_Property is IntegerProperty ip) return ip.Unit;
if (_Property is DoubleProperty dp) return dp.Unit;
return null;
}
}
public List<string> Options
{
get
{
if (_Property != null && _Property is StringProperty sp)
{
return sp.Options;
}
return null;
}
}
List<Data> _Children;
public List<Data> Children
{
get
{
if (_Children != null) return _Children;
if(_Property is ICollectionProperty cp && cp.Children.Count > 0)
{
_Children = new List<Data>();
foreach(var child in cp.Children)
{
var childData = new Data(child);
childData.ValueChanged = ValueChanged;
_Children.Add(childData);
}
return _Children;
}
return null;
}
}
}
IProperty接口
public interface IProperty
{
string Name { get; set; }
string Description { get; set; }
DataType DataType { get; }
string Category { get; set; }
bool Visible { get; set; }
bool NotNull { get; set;}
bool Editable { get; set; }
object Value { get; set; }
string StringValue { get; set; }
IProperty Clone();
}
TypeDef Editor工具
一个json中可以定义多个类型,每个类型可以对应一种业务对象,类型下面就是属性了。
最后,PropertyGrid运行效果