Skip to content

Commit 1626041

Browse files
committed
重构代码,复用。修改文档。实现colorpicker。
1 parent d92f4a0 commit 1626041

21 files changed

Lines changed: 643 additions & 681 deletions

File tree

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using System.Reflection;
2+
using AutoSettingUI.Core.Attributes;
3+
using AutoSettingUI.Core.Models;
4+
5+
namespace AutoSettingUI.Extension.Shared;
6+
7+
/// <summary>
8+
/// Shared helper for creating extended controls from ControlBindingAttribute.
9+
/// Used by Avalonia and Ursa implementations.
10+
/// </summary>
11+
public static class ExtendedControlHelper
12+
{
13+
/// <summary>
14+
/// Creates a control instance from a ControlBindingAttribute.
15+
/// </summary>
16+
/// <param name="attr">The control binding attribute.</param>
17+
/// <param name="propertyType">The type of the property being bound.</param>
18+
/// <returns>The created control, or null if creation fails.</returns>
19+
public static object? CreateControl(ControlBindingAttribute attr, Type propertyType)
20+
{
21+
if (attr.ControlType == null)
22+
return null;
23+
24+
var controlType = attr.ControlType;
25+
object? control = null;
26+
27+
if (!string.IsNullOrEmpty(attr.FactoryMethod))
28+
{
29+
control = TryInvokeFactoryMethod(attr, controlType, propertyType);
30+
}
31+
32+
control ??= Activator.CreateInstance(controlType);
33+
34+
return control;
35+
}
36+
37+
/// <summary>
38+
/// Gets the AvaloniaProperty for binding from a control type.
39+
/// </summary>
40+
/// <param name="controlType">The control type.</param>
41+
/// <param name="bindingPropertyName">The name of the binding property (without "Property" suffix).</param>
42+
/// <returns>The AvaloniaProperty field info, or null if not found.</returns>
43+
public static FieldInfo? GetBindingPropertyField(Type controlType, string bindingPropertyName)
44+
{
45+
if (string.IsNullOrEmpty(bindingPropertyName))
46+
return null;
47+
48+
return controlType.GetField(
49+
bindingPropertyName + "Property",
50+
BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy);
51+
}
52+
53+
/// <summary>
54+
/// Checks if a property has a ControlBindingAttribute.
55+
/// </summary>
56+
public static bool HasControlBindingAttribute(PropertyInfo propertyInfo)
57+
{
58+
return propertyInfo.GetCustomAttribute<ControlBindingAttribute>() != null;
59+
}
60+
61+
/// <summary>
62+
/// Gets the ControlBindingAttribute from a property.
63+
/// </summary>
64+
public static ControlBindingAttribute? GetControlBindingAttribute(PropertyInfo propertyInfo)
65+
{
66+
return propertyInfo.GetCustomAttribute<ControlBindingAttribute>();
67+
}
68+
69+
private static object? TryInvokeFactoryMethod(ControlBindingAttribute attr, Type controlType, Type propertyType)
70+
{
71+
var method = controlType.GetMethod(attr.FactoryMethod!,
72+
BindingFlags.Static | BindingFlags.Public);
73+
74+
if (method != null)
75+
{
76+
var parameters = method.GetParameters();
77+
if (parameters.Length == 0)
78+
return method.Invoke(null, null);
79+
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(Type))
80+
return method.Invoke(null, new object[] { propertyType });
81+
}
82+
83+
var attrMethod = attr.GetType().GetMethod(attr.FactoryMethod!,
84+
BindingFlags.Instance | BindingFlags.Public);
85+
86+
if (attrMethod != null)
87+
{
88+
var parameters = attrMethod.GetParameters();
89+
if (parameters.Length == 0)
90+
return attrMethod.Invoke(attr, null);
91+
if (parameters.Length == 1 && parameters[0].ParameterType == typeof(Type))
92+
return attrMethod.Invoke(attr, new object[] { propertyType });
93+
}
94+
95+
return null;
96+
}
97+
}
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
using System.Reflection;
2+
using System.Text.RegularExpressions;
3+
using AutoSettingUI.Core.Attributes;
4+
5+
namespace AutoSettingUI.Extension.Shared;
6+
7+
/// <summary>
8+
/// Shared validation logic for property values across all UI frameworks.
9+
/// </summary>
10+
public static class ValidationHelper
11+
{
12+
/// <summary>
13+
/// Gets validation attributes from a property.
14+
/// </summary>
15+
public static ValidationAttribute[] GetValidations(PropertyInfo property)
16+
{
17+
return property.GetCustomAttributes<ValidationAttribute>().ToArray();
18+
}
19+
20+
/// <summary>
21+
/// Validates a value against the validation attributes of a property.
22+
/// </summary>
23+
/// <param name="property">The property being validated.</param>
24+
/// <param name="value">The value to validate.</param>
25+
/// <param name="target">The target object containing the property.</param>
26+
/// <param name="errorMessage">Output error message if validation fails.</param>
27+
/// <returns>True if validation passes, false otherwise.</returns>
28+
public static bool ValidateValue(PropertyInfo property, object? value, object target, out string? errorMessage)
29+
{
30+
errorMessage = null;
31+
var validations = GetValidations(property);
32+
33+
foreach (var validation in validations)
34+
{
35+
if (!ValidateSingle(validation, property, value, target, out errorMessage))
36+
return false;
37+
}
38+
39+
return true;
40+
}
41+
42+
/// <summary>
43+
/// Validates a single validation attribute.
44+
/// </summary>
45+
private static bool ValidateSingle(ValidationAttribute validation, PropertyInfo property, object? value, object target, out string? errorMessage)
46+
{
47+
errorMessage = null;
48+
49+
if (validation.Required && IsNullOrEmpty(value))
50+
{
51+
errorMessage = validation.ErrorMessage ?? $"{property.Name} is required.";
52+
return false;
53+
}
54+
55+
if (value is string stringValue && !string.IsNullOrEmpty(stringValue))
56+
{
57+
if (!ValidateStringLength(validation, stringValue, property, out errorMessage))
58+
return false;
59+
60+
if (!ValidatePattern(validation, stringValue, property, out errorMessage))
61+
return false;
62+
}
63+
64+
var actualValue = TryConvertValue(value, property.PropertyType);
65+
if (!ValidateRange(validation, actualValue, property, out errorMessage))
66+
return false;
67+
68+
if (!ValidateCustomMethod(validation, actualValue, target, property, out errorMessage))
69+
return false;
70+
71+
return true;
72+
}
73+
74+
private static bool IsNullOrEmpty(object? value)
75+
{
76+
return value == null || (value is string str && string.IsNullOrWhiteSpace(str));
77+
}
78+
79+
private static bool ValidateStringLength(ValidationAttribute validation, string value, PropertyInfo property, out string? errorMessage)
80+
{
81+
errorMessage = null;
82+
83+
if (validation.MinLength >= 0 && value.Length < validation.MinLength)
84+
{
85+
errorMessage = validation.ErrorMessage ?? $"{property.Name} must be at least {validation.MinLength} characters.";
86+
return false;
87+
}
88+
89+
if (validation.MaxLength >= 0 && value.Length > validation.MaxLength)
90+
{
91+
errorMessage = validation.ErrorMessage ?? $"{property.Name} must be at most {validation.MaxLength} characters.";
92+
return false;
93+
}
94+
95+
return true;
96+
}
97+
98+
private static bool ValidatePattern(ValidationAttribute validation, string value, PropertyInfo property, out string? errorMessage)
99+
{
100+
errorMessage = null;
101+
102+
if (!string.IsNullOrEmpty(validation.Pattern) && !Regex.IsMatch(value, validation.Pattern))
103+
{
104+
errorMessage = validation.ErrorMessage ?? $"{property.Name} format is invalid.";
105+
return false;
106+
}
107+
108+
return true;
109+
}
110+
111+
private static bool ValidateRange(ValidationAttribute validation, object? value, PropertyInfo property, out string? errorMessage)
112+
{
113+
errorMessage = null;
114+
115+
if (value is not IComparable comparable)
116+
return true;
117+
118+
if (!double.IsNaN(validation.MinValue))
119+
{
120+
try
121+
{
122+
var min = Convert.ChangeType(validation.MinValue, comparable.GetType());
123+
if (comparable.CompareTo(min) < 0)
124+
{
125+
errorMessage = validation.ErrorMessage ?? $"{property.Name} must be at least {validation.MinValue}.";
126+
return false;
127+
}
128+
}
129+
catch { }
130+
}
131+
132+
if (!double.IsNaN(validation.MaxValue))
133+
{
134+
try
135+
{
136+
var max = Convert.ChangeType(validation.MaxValue, comparable.GetType());
137+
if (comparable.CompareTo(max) > 0)
138+
{
139+
errorMessage = validation.ErrorMessage ?? $"{property.Name} must be at most {validation.MaxValue}.";
140+
return false;
141+
}
142+
}
143+
catch { }
144+
}
145+
146+
return true;
147+
}
148+
149+
private static bool ValidateCustomMethod(ValidationAttribute validation, object? value, object target, PropertyInfo property, out string? errorMessage)
150+
{
151+
errorMessage = null;
152+
153+
if (string.IsNullOrEmpty(validation.ValidateMethod))
154+
return true;
155+
156+
var method = target.GetType().GetMethod(validation.ValidateMethod,
157+
BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static);
158+
159+
if (method == null)
160+
return true;
161+
162+
var result = method.IsStatic
163+
? method.Invoke(null, new[] { value })
164+
: method.Invoke(target, new[] { value });
165+
166+
if (result is bool isValid && !isValid)
167+
{
168+
errorMessage = validation.ErrorMessage ?? $"{property.Name} is invalid.";
169+
return false;
170+
}
171+
172+
return true;
173+
}
174+
175+
private static object? TryConvertValue(object? value, Type targetType)
176+
{
177+
if (value is not string stringValue || string.IsNullOrWhiteSpace(stringValue))
178+
return value;
179+
180+
if (targetType == typeof(string))
181+
return value;
182+
183+
try
184+
{
185+
return Convert.ChangeType(stringValue, targetType);
186+
}
187+
catch
188+
{
189+
return value;
190+
}
191+
}
192+
}

Demo/AutoSettingUI.Avalonia.CrossPlatform.Demo/AutoSettingUI.Avalonia.CrossPlatform.Demo/App.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
</Application.DataTemplates>
1111
<Application.Styles>
1212
<FluentTheme />
13+
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml"/>
1314
<StyleInclude Source="avares://AutoSettingUI.Avalonia/Themes/Generic.axaml"/>
1415
</Application.Styles>
1516
</Application>

Demo/AutoSettingUI.Avalonia.CrossPlatform.Demo/AutoSettingUI.Avalonia.CrossPlatform.Demo/Models/ApplicationSettings.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
34
using System.ComponentModel;
45
using AutoSettingUI.Core.Attributes;
56
using AutoSettingUI.Avalonia.Attributes;
@@ -187,18 +188,18 @@ public string Email
187188

188189
[SubHeader("Collection Examples")]
189190
[Title("Tags (Default Collection Editor)")]
190-
public List<string> Tags { get; set; } = new List<string> { "Important", "Work" };
191+
public ObservableCollection<string> Tags { get; set; } = ["Important", "Work"];
191192

192193
[Title("Versions (Read-Only Collection)")]
193194
[CollectionEditor(AllowAdd = false, AllowRemove = false, AllowReorder = false)]
194-
public List<string> Versions { get; set; } = new List<string> { "1.0.0", "1.1.0", "2.0.0" };
195+
public ObservableCollection<string> Versions { get; set; } = ["1.0.0", "1.1.0", "2.0.0"];
195196

196197
[Title("People (Complex Collection)")]
197-
public List<Person> People { get; set; } = new List<Person>
198-
{
198+
public ObservableCollection<Person> People { get; set; } =
199+
[
199200
new Person { Name = "John Doe", Age = 30, Email = "john@example.com" },
200201
new Person { Name = "Jane Smith", Age = 25, Email = "jane@example.com" }
201-
};
202+
];
202203

203204
public UserPreferences()
204205
{
@@ -378,7 +379,7 @@ public AppTheme SelectedTheme
378379
}
379380
}
380381

381-
public static List<AppTheme> AvailableThemes => new()
382+
public static ObservableCollection<AppTheme> AvailableThemes => new()
382383
{
383384
AppTheme.Default,
384385
AppTheme.Light,

Demo/AutoSettingUI.Ursa.Demo/App.axaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@
1919
<semi:SemiTheme></semi:SemiTheme>
2020
<!-- <FluentTheme /> -->
2121
<!-- Semi and Ursa Themes -->
22-
23-
22+
<StyleInclude Source="avares://Avalonia.Controls.ColorPicker/Themes/Fluent/Fluent.xaml"/>
2423
<StyleInclude Source="avares://AutoSettingUI.Ursa/Themes/Generic.axaml"/>
2524
</Application.Styles>
2625

Demo/AutoSettingUI.Ursa.Demo/Models/ApplicationSettings.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.ObjectModel;
34
using System.ComponentModel;
45
using System.Net;
56
using AutoSettingUI.Core.Attributes;
@@ -191,18 +192,18 @@ public string Email
191192

192193
[SubHeader("Collection Examples")]
193194
[Title("Tags (Default Collection Editor)")]
194-
public List<string> Tags { get; set; } = new List<string> { "Important", "Work" };
195+
public ObservableCollection<string> Tags { get; set; } = [ "Important", "Work" ];
195196

196197
[Title("Versions (Read-Only Collection)")]
197198
[CollectionEditor(AllowAdd = false, AllowRemove = false, AllowReorder = false)]
198-
public List<string> Versions { get; set; } = new List<string> { "1.0.0", "1.1.0", "2.0.0" };
199+
public ObservableCollection<string> Versions { get; set; } = ["1.0.0", "1.1.0", "2.0.0"];
199200

200201
[Title("People (Complex Collection)")]
201-
public List<Person> People { get; set; } = new List<Person>
202-
{
202+
public ObservableCollection<Person> People { get; set; } =
203+
[
203204
new Person { Name = "John Doe", Age = 30, Email = "john@example.com" },
204205
new Person { Name = "Jane Smith", Age = 25, Email = "jane@example.com" }
205-
};
206+
];
206207

207208
public UserPreferences()
208209
{
@@ -333,7 +334,7 @@ public class ExtendedControlsSettings
333334

334335
[Title("Selection Tags")]
335336
[TagInput]
336-
public List<string> ProjectTags { get; set; } = new() { "Ursa", "Avalonia", "AutoSettingUI" };
337+
public ObservableCollection<string> ProjectTags { get; set; } = ["Ursa", "Avalonia", "AutoSettingUI"];
337338

338339
[Title("Release Date")]
339340
[DatePicker]

0 commit comments

Comments
 (0)