better web settings

This commit is contained in:
EugeneTes
2026-03-19 09:13:09 +01:00
parent 83597b8763
commit 810c7053c0
4 changed files with 367 additions and 6 deletions

View File

@@ -0,0 +1,118 @@
@using MudBlazor
@using System.Reflection
<MudDialog>
<DialogContent>
@if (_isSimpleType)
{
@RenderEditor(ItemType, _workingValue, v => _workingValue = v, "Value")
}
else
{
@foreach (var prop in _properties)
{
var p = prop;
var currentVal = p.GetValue(_workingValue);
@RenderEditor(p.PropertyType, currentVal, v => p.SetValue(_workingValue, v), p.Name)
}
}
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Ok">OK</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public Type ItemType { get; set; } = null!;
[Parameter] public object? InitialValue { get; set; }
private object? _workingValue;
private bool _isSimpleType;
private PropertyInfo[] _properties = Array.Empty<PropertyInfo>();
protected override void OnParametersSet()
{
_isSimpleType = ItemType.IsValueType || ItemType == typeof(string) || ItemType.IsEnum;
_workingValue = InitialValue;
if (!_isSimpleType && _workingValue != null)
{
_properties = ItemType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead && p.CanWrite)
.ToArray();
}
}
private RenderFragment RenderEditor(Type type, object? value, Action<object?> onChanged, string label) => builder =>
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "settings-field");
if (type == typeof(bool))
{
var boolVal = (bool)(value ?? false);
builder.OpenComponent<MudSwitch<bool>>(2);
builder.AddAttribute(3, "Value", boolVal);
builder.AddAttribute(4, "ValueChanged", EventCallback.Factory.Create<bool>(this, v => { onChanged(v); StateHasChanged(); }));
builder.AddAttribute(5, "Label", label);
builder.AddAttribute(6, "Color", Color.Primary);
builder.CloseComponent();
}
else if (type == typeof(int))
{
var intVal = value is int iv ? iv : 0;
builder.OpenComponent<MudNumericField<int>>(2);
builder.AddAttribute(3, "Value", intVal);
builder.AddAttribute(4, "ValueChanged", EventCallback.Factory.Create<int>(this, v => { onChanged(v); StateHasChanged(); }));
builder.AddAttribute(5, "Label", label);
builder.AddAttribute(6, "Variant", Variant.Outlined);
builder.CloseComponent();
}
else if (type == typeof(double))
{
var dblVal = value is double dv ? dv : 0.0;
builder.OpenComponent<MudNumericField<double>>(2);
builder.AddAttribute(3, "Value", dblVal);
builder.AddAttribute(4, "ValueChanged", EventCallback.Factory.Create<double>(this, v => { onChanged(v); StateHasChanged(); }));
builder.AddAttribute(5, "Label", label);
builder.AddAttribute(6, "Variant", Variant.Outlined);
builder.CloseComponent();
}
else if (type.IsEnum)
{
builder.OpenComponent<MudSelect<object>>(2);
builder.AddAttribute(3, "Value", value);
builder.AddAttribute(4, "ValueChanged", EventCallback.Factory.Create<object>(this, v => { onChanged(v); StateHasChanged(); }));
builder.AddAttribute(5, "Label", label);
builder.AddAttribute(6, "Variant", Variant.Outlined);
builder.AddAttribute(7, "ChildContent", (RenderFragment)(innerBuilder =>
{
foreach (var enumVal in Enum.GetValues(type))
{
innerBuilder.OpenComponent<MudSelectItem<object>>(0);
innerBuilder.AddAttribute(1, "Value", enumVal);
innerBuilder.AddAttribute(2, "ChildContent", (RenderFragment)(b => b.AddContent(0, enumVal.ToString())));
innerBuilder.CloseComponent();
}
}));
builder.CloseComponent();
}
else
{
var strVal = value?.ToString() ?? "";
builder.OpenComponent<MudTextField<string>>(2);
builder.AddAttribute(3, "Value", strVal);
builder.AddAttribute(4, "ValueChanged", EventCallback.Factory.Create<string>(this, v => { onChanged(v); StateHasChanged(); }));
builder.AddAttribute(5, "Label", label);
builder.AddAttribute(6, "Variant", Variant.Outlined);
builder.CloseComponent();
}
builder.CloseElement();
};
private void Ok() => MudDialog.Close(DialogResult.Ok(_workingValue));
private void Cancel() => MudDialog.Cancel();
}

View File

@@ -0,0 +1,152 @@
@using MudBlazor
@using System.Collections
@using System.Reflection
<MudDialog>
<DialogContent>
<div class="list-editor-layout">
<div class="list-editor-list">
<MudList T="int" Dense="true" @bind-SelectedValue="_selectedIndex">
@for (int i = 0; i < _items.Count; i++)
{
var idx = i;
<MudListItem T="int" Value="@idx"
Text="@(_items[idx]?.ToString() ?? "(null)")" />
}
</MudList>
</div>
<div class="list-editor-buttons">
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="Add" FullWidth="true">Add</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="Edit" FullWidth="true"
Disabled="@(_selectedIndex < 0)">Edit</MudButton>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="Remove" FullWidth="true"
Disabled="@(_selectedIndex < 0)">Remove</MudButton>
</div>
</div>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Ok">OK</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public Type ElementType { get; set; } = null!;
[Parameter] public IList? InitialList { get; set; }
[Microsoft.AspNetCore.Components.Inject] private IDialogService DialogService { get; set; } = null!;
private List<object?> _items = new();
private int _selectedIndex = -1;
protected override void OnParametersSet()
{
_items.Clear();
if (InitialList != null)
{
foreach (var item in InitialList)
_items.Add(CloneObject(item));
}
}
private async Task Add()
{
var defaultValue = GetDefaultValue(ElementType);
var result = await EditItem(defaultValue);
if (result != null)
{
_items.Add(result);
_selectedIndex = _items.Count - 1;
}
}
private async Task Edit()
{
if (_selectedIndex < 0 || _selectedIndex >= _items.Count) return;
var result = await EditItem(_items[_selectedIndex]);
if (result != null)
{
_items[_selectedIndex] = result;
}
}
private void Remove()
{
if (_selectedIndex < 0 || _selectedIndex >= _items.Count) return;
_items.RemoveAt(_selectedIndex);
if (_items.Count > 0)
_selectedIndex = Math.Min(_selectedIndex, _items.Count - 1);
else
_selectedIndex = -1;
}
private async Task<object?> EditItem(object? value)
{
if (ElementType.IsValueType || ElementType == typeof(string) || ElementType.IsEnum)
{
// Simple type: inline edit via ItemEditorDialog
var parameters = new DialogParameters<ItemEditorDialog>
{
{ x => x.ItemType, ElementType },
{ x => x.InitialValue, value }
};
var dialog = await DialogService.ShowAsync<ItemEditorDialog>($"Edit {ElementType.Name}", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled)
return result.Data;
return null;
}
else
{
// Complex type: edit properties
var parameters = new DialogParameters<ItemEditorDialog>
{
{ x => x.ItemType, ElementType },
{ x => x.InitialValue, CloneObject(value) }
};
var dialog = await DialogService.ShowAsync<ItemEditorDialog>($"Edit {ElementType.Name}", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled)
return result.Data;
return null;
}
}
private void Ok()
{
var listType = typeof(List<>).MakeGenericType(ElementType);
var resultList = (IList)Activator.CreateInstance(listType)!;
foreach (var item in _items)
resultList.Add(item);
MudDialog.Close(DialogResult.Ok(resultList));
}
private void Cancel() => MudDialog.Cancel();
private object? GetDefaultValue(Type type)
{
if (type.IsValueType) return Activator.CreateInstance(type);
if (type == typeof(string)) return "";
try { return Activator.CreateInstance(type); }
catch { return null; }
}
private object? CloneObject(object? obj)
{
if (obj == null) return null;
var type = obj.GetType();
if (type.IsValueType || type == typeof(string)) return obj;
try
{
var clone = Activator.CreateInstance(type);
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (prop.CanRead && prop.CanWrite)
prop.SetValue(clone, prop.GetValue(obj));
}
return clone;
}
catch { return obj; }
}
}

View File

@@ -1,7 +1,9 @@
@using MudBlazor @using MudBlazor
@using System.Collections
@using System.ComponentModel @using System.ComponentModel
@using System.Reflection @using System.Reflection
@using Inspectron.Settings @using Inspectron.Settings
@using Inspectron.Settings.Attributes
<MudDialog> <MudDialog>
<DialogContent> <DialogContent>
@@ -17,6 +19,11 @@
@foreach (var setting in settings) @foreach (var setting in settings)
{ {
<div class="settings-field"> <div class="settings-field">
@if (!string.IsNullOrEmpty(setting.SettingDescription))
{
<MudText Typo="Typo.caption" Class="mb-1" Style="font-style: italic;">@setting.SettingDescription</MudText>
}
@if (setting.Property.PropertyType == typeof(bool)) @if (setting.Property.PropertyType == typeof(bool))
{ {
var boolVal = (bool)(setting.Value ?? false); var boolVal = (bool)(setting.Value ?? false);
@@ -49,6 +56,15 @@
} }
</MudSelect> </MudSelect>
} }
else if (IsListType(setting.Property.PropertyType))
{
var s = setting;
<MudText Typo="Typo.subtitle2" Class="mb-1">@setting.Description</MudText>
<MudTextField T="string" Value="@GetListDisplayText(setting.Value as IList)"
ReadOnly="true" Lines="3" Variant="Variant.Outlined" />
<MudButton Variant="Variant.Outlined" Color="Color.Primary" Class="mt-1"
OnClick="@(() => EditList(s))">Edit</MudButton>
}
else else
{ {
var strVal = setting.Value?.ToString() ?? ""; var strVal = setting.Value?.ToString() ?? "";
@@ -76,6 +92,8 @@
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!; [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public InspectronSettings? Settings { get; set; } [Parameter] public InspectronSettings? Settings { get; set; }
[Microsoft.AspNetCore.Components.Inject] private IDialogService DialogService { get; set; } = null!;
private Dictionary<string, List<SettingEntry>> _categories = new(); private Dictionary<string, List<SettingEntry>> _categories = new();
private string? _selectedCategory; private string? _selectedCategory;
private List<CategoryNode> _treeRoot = new(); private List<CategoryNode> _treeRoot = new();
@@ -95,7 +113,6 @@
if (root != null) if (root != null)
Traverse(root, ""); Traverse(root, "");
// Build the UI tree from category paths
foreach (var categoryPath in _categories.Keys) foreach (var categoryPath in _categories.Keys)
{ {
var parts = categoryPath.Split('/'); var parts = categoryPath.Split('/');
@@ -137,11 +154,18 @@
{ {
var owner = bpd.Owner; var owner = bpd.Owner;
var description = string.IsNullOrEmpty(pd.Description) ? bpd.DisplayName : pd.Description; var description = string.IsNullOrEmpty(pd.Description) ? bpd.DisplayName : pd.Description;
string? settingDesc = null;
var descAttr = bpd.PropertyInfo.GetCustomAttribute<SettingDescriptionAttribute>();
if (descAttr != null)
settingDesc = descAttr.Description;
var entry = new SettingEntry var entry = new SettingEntry
{ {
Owner = owner, Owner = owner,
Property = bpd.PropertyInfo, Property = bpd.PropertyInfo,
Description = description, Description = description,
SettingDescription = settingDesc,
Value = bpd.PropertyInfo.GetValue(owner) Value = bpd.PropertyInfo.GetValue(owner)
}; };
@@ -163,11 +187,9 @@
private List<SettingEntry> GetSettingsForCategory(string category) private List<SettingEntry> GetSettingsForCategory(string category)
{ {
// Exact match first
if (_categories.TryGetValue(category, out var exact)) if (_categories.TryGetValue(category, out var exact))
return exact; return exact;
// Otherwise aggregate all sub-categories
var prefix = category + "/"; var prefix = category + "/";
return _categories return _categories
.Where(kvp => kvp.Key.StartsWith(prefix)) .Where(kvp => kvp.Key.StartsWith(prefix))
@@ -175,6 +197,39 @@
.ToList(); .ToList();
} }
private static bool IsListType(Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>);
}
private static string GetListDisplayText(IList? list)
{
if (list == null || list.Count == 0)
return "(empty list)";
var items = new List<string>();
foreach (var item in list)
items.Add(item?.ToString() ?? "(null)");
return string.Join(Environment.NewLine, items);
}
private async Task EditList(SettingEntry setting)
{
var elementType = setting.Property.PropertyType.GetGenericArguments()[0];
var parameters = new DialogParameters<ListEditorDialog>
{
{ x => x.ElementType, elementType },
{ x => x.InitialList, setting.Value as IList }
};
var options = new DialogOptions { MaxWidth = MaxWidth.Medium, FullWidth = true };
var dialog = await DialogService.ShowAsync<ListEditorDialog>($"Edit {setting.Description}", parameters, options);
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is IList newList)
{
setting.Value = newList;
StateHasChanged();
}
}
private void Save() private void Save()
{ {
foreach (var list in _categories.Values) foreach (var list in _categories.Values)
@@ -186,7 +241,22 @@
{ {
try try
{ {
if (setting.Value != null && setting.Value.GetType() != setting.Property.PropertyType) if (IsListType(setting.Property.PropertyType) && setting.Value is IList listVal)
{
// For list types, clear and repopulate the original list
var originalList = setting.Property.GetValue(setting.Owner) as IList;
if (originalList != null)
{
originalList.Clear();
foreach (var item in listVal)
originalList.Add(item);
}
else
{
setting.Property.SetValue(setting.Owner, setting.Value);
}
}
else if (setting.Value != null && setting.Value.GetType() != setting.Property.PropertyType)
{ {
var converted = Convert.ChangeType(setting.Value, setting.Property.PropertyType); var converted = Convert.ChangeType(setting.Value, setting.Property.PropertyType);
setting.Property.SetValue(setting.Owner, converted); setting.Property.SetValue(setting.Owner, converted);
@@ -198,7 +268,6 @@
} }
catch catch
{ {
// Skip properties that fail to convert
} }
} }
} }
@@ -214,6 +283,7 @@
public object Owner { get; set; } = null!; public object Owner { get; set; } = null!;
public PropertyInfo Property { get; set; } = null!; public PropertyInfo Property { get; set; } = null!;
public string Description { get; set; } = ""; public string Description { get; set; } = "";
public string? SettingDescription { get; set; }
public object? Value { get; set; } public object? Value { get; set; }
} }

View File

@@ -289,5 +289,26 @@
} }
.settings-field { .settings-field {
margin-bottom: 12px; margin-bottom: 16px;
}
/* List Editor Dialog */
.list-editor-layout {
display: flex;
gap: 12px;
min-height: 300px;
}
.list-editor-list {
flex: 1;
border: 1px solid #e0e0e0;
overflow-y: auto;
max-height: 350px;
}
.list-editor-buttons {
display: flex;
flex-direction: column;
gap: 8px;
min-width: 80px;
} }