settings for avalonia

libcamera bugfix
This commit is contained in:
EugeneTes
2026-04-01 14:05:37 +02:00
parent 1032a5a28c
commit bb861834fd
26 changed files with 1139 additions and 59 deletions

View File

@@ -3,11 +3,11 @@ using VisionBuilder.UI.Common;
namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera; namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera;
public class CaptureSettings : ISettings public class LibcameraCaptureSettings : ISettings
{ {
public string CameraName { get; } public string CameraName { get; }
public CaptureSettings(string cameraName) public LibcameraCaptureSettings(string cameraName)
{ {
CameraName = cameraName; CameraName = cameraName;
} }

View File

@@ -8,9 +8,9 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera
{ {
public class LibcameraImageSource : IImageSource, IVisionBuilderModule public class LibcameraImageSource : IImageSource, IVisionBuilderModule
{ {
private CaptureSettings _settings; private LibcameraCaptureSettings _settings;
public LibcameraImageSource(CaptureSettings settings) public LibcameraImageSource(LibcameraCaptureSettings settings)
{ {
_settings = settings; _settings = settings;
} }
@@ -40,6 +40,8 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera
} }
private void StartLoop() private void StartLoop()
{
try
{ {
var psi = new ProcessStartInfo var psi = new ProcessStartInfo
{ {
@@ -91,6 +93,7 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera
Array.Copy(temp, 0, buff, buff.Length - imageEnd, temp.Length); Array.Copy(temp, 0, buff, buff.Length - imageEnd, temp.Length);
break; break;
} }
// copy all of the data to the imageBuffer // copy all of the data to the imageBuffer
Array.Copy(buff, 0, imageBuffer, size, buff.Length); Array.Copy(buff, 0, imageBuffer, size, buff.Length);
size += buff.Length; size += buff.Length;
@@ -109,6 +112,11 @@ namespace Hawkeye.VisionBuilder.UI.Sources.Raspberry.Libcamera
} }
} }
catch (Exception e)
{
Serilog.Log.Error($"Error starting libcamera: {e}");
}
}
public static int Find(byte[] buff, byte[] search) public static int Find(byte[] buff, byte[] search)
{ {

View File

@@ -7,6 +7,7 @@ using Ninject.Extensions.ChildKernel;
using VisionBuilder.UI.Common; using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Plugins; using VisionBuilder.UI.Common.Plugins;
using VisionBuilder.UI.Common.Processing; using VisionBuilder.UI.Common.Processing;
using VisionBuilder.UI.Common.Services;
using VisionBuilder.UI.Common.ViewModel; using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.IOCommander; using VisionBuilder.UI.IOCommander;
using VisionBuilder.UI.Recipes.HawkeyeRecipe; using VisionBuilder.UI.Recipes.HawkeyeRecipe;
@@ -15,6 +16,7 @@ using VisionBuilder.UI.Statistics;
using VisionBuilder.UI.Avalonia; using VisionBuilder.UI.Avalonia;
using VisionBuilder.UI.Avalonia.Uno.Views; using VisionBuilder.UI.Avalonia.Uno.Views;
using Lindt.Colorballs.Duo.Utils; using Lindt.Colorballs.Duo.Utils;
using VisionBuilder.UI.Camera;
namespace VisionBuilder.UI.Avalonia.Uno; namespace VisionBuilder.UI.Avalonia.Uno;
@@ -79,10 +81,12 @@ public class App : Application
]; ];
var avaloniaUiSettings = mainKernel.Get<AvaloniaUISettings>(); var avaloniaUiSettings = mainKernel.Get<AvaloniaUISettings>();
var uiConfiguration = mainKernel.Get<UIConfiguration>();
var passwordInputService = mainKernel.Get<IPasswordInputService>();
settings.LoadSettings(); settings.LoadSettings();
desktop.MainWindow = new MainWindow(mainWindowVm, avaloniaUiSettings); desktop.MainWindow = new MainWindow(mainWindowVm, avaloniaUiSettings, uiConfiguration, passwordInputService);
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();

View File

@@ -1,5 +1,6 @@
using Avalonia; using Avalonia;
using Avalonia.LinuxFramebuffer; using Avalonia.LinuxFramebuffer;
using Serilog;
namespace VisionBuilder.UI.Avalonia.Uno; namespace VisionBuilder.UI.Avalonia.Uno;
@@ -8,6 +9,11 @@ public class Program
[STAThread] [STAThread]
public static void Main(string[] args) public static void Main(string[] args)
{ {
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.Console()
.CreateLogger();
if (args.Contains("--drm")) if (args.Contains("--drm"))
BuildAvaloniaApp().StartLinuxDrm(args, card: null, scaling: 1.0); BuildAvaloniaApp().StartLinuxDrm(args, card: null, scaling: 1.0);
else else

View File

@@ -20,6 +20,16 @@
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" VerticalAlignment="Center"
Spacing="8"> Spacing="8">
<Button x:Name="BtnSettings"
Content="SETTINGS"
Width="176" Height="48"
FontSize="14" FontWeight="Bold"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
BorderBrush="#D32F2F"
BorderThickness="2"
Background="White"
Foreground="#D32F2F" />
<Button x:Name="BtnMinimize" <Button x:Name="BtnMinimize"
Content="MINIMIZE" Content="MINIMIZE"
Width="176" Height="48" Width="176" Height="48"

View File

@@ -8,6 +8,8 @@ using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading; using Avalonia.Threading;
using VisionBuilder.UI.Avalonia; using VisionBuilder.UI.Avalonia;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Services;
using VisionBuilder.UI.Common.ViewModel; using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.Common.ViewModel.Classes; using VisionBuilder.UI.Common.ViewModel.Classes;
@@ -17,12 +19,17 @@ public partial class MainWindow : Window
{ {
private readonly MainWindowVM _mainWindowVm; private readonly MainWindowVM _mainWindowVm;
private readonly AvaloniaUISettings _avaloniaUiSettings; private readonly AvaloniaUISettings _avaloniaUiSettings;
private readonly UIConfiguration _uiConfiguration;
private readonly IPasswordInputService _passwordInputService;
private readonly Dictionary<string, Control> _dynamicButtonControls = new(); private readonly Dictionary<string, Control> _dynamicButtonControls = new();
public MainWindow(MainWindowVM mainWindowVm, AvaloniaUISettings avaloniaUiSettings) public MainWindow(MainWindowVM mainWindowVm, AvaloniaUISettings avaloniaUiSettings,
UIConfiguration uiConfiguration, IPasswordInputService passwordInputService)
{ {
_mainWindowVm = mainWindowVm; _mainWindowVm = mainWindowVm;
_avaloniaUiSettings = avaloniaUiSettings; _avaloniaUiSettings = avaloniaUiSettings;
_uiConfiguration = uiConfiguration;
_passwordInputService = passwordInputService;
InitializeComponent(); InitializeComponent();
@@ -30,6 +37,7 @@ public partial class MainWindow : Window
SingleCameraView.SetViewModel(_mainWindowVm.SingleCameraVms[0]); SingleCameraView.SetViewModel(_mainWindowVm.SingleCameraVms[0]);
BtnSettings.Click += BtnSettings_Click;
BtnMinimize.Click += BtnMinimize_Click; BtnMinimize.Click += BtnMinimize_Click;
BtnExit.Click += BtnExit_Click; BtnExit.Click += BtnExit_Click;
@@ -80,6 +88,17 @@ public partial class MainWindow : Window
} }
} }
private async void BtnSettings_Click(object? sender, RoutedEventArgs e)
{
if (_uiConfiguration.ProtectSettingsWithPassword)
{
var passwordOk = await _passwordInputService.GetPasswordAsync() == _uiConfiguration.AdminPassword;
if (!passwordOk)
return;
}
_mainWindowVm.SettingsCommand.Execute(null);
}
private void BtnMinimize_Click(object? sender, RoutedEventArgs e) private void BtnMinimize_Click(object? sender, RoutedEventArgs e)
{ {
WindowState = WindowState.Minimized; WindowState = WindowState.Minimized;

View File

@@ -21,6 +21,7 @@
<PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu.Windows" Version="1.18.0" ExcludeAssets="all" /> <PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu.Windows" Version="1.18.0" ExcludeAssets="all" />
<PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu.Linux" Version="1.18.0" ExcludeAssets="all" /> <PackageReference Include="Microsoft.ML.OnnxRuntime.Gpu.Linux" Version="1.18.0" ExcludeAssets="all" />
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.18.0" /> <PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.18.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,11 +1,38 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Inspectron.Settings;
using Inspectron.Settings.Avalonia.Configuration;
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI; using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
namespace VisionBuilder.UI.Avalonia.Services; namespace VisionBuilder.UI.Avalonia.Services;
public class AvaloniaSettingsService : ISettingsService public class AvaloniaSettingsService : ISettingsService
{ {
public Task ShowSettingsDialogAsync() private readonly InspectronSettings _settings;
public AvaloniaSettingsService(InspectronSettings settings)
{ {
return Task.CompletedTask; _settings = settings;
}
public async Task ShowSettingsDialogAsync()
{
var dialog = new OptionsWindow(new DefaultControlFactory());
dialog.LoadFromSettings(_settings);
var parent = GetMainWindow();
if (parent != null)
{
var result = await dialog.ShowDialog<bool>(parent);
if (result)
_settings.SaveSettings();
}
}
private static Window? GetMainWindow()
{
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
return desktop.MainWindow;
return null;
} }
} }

View File

@@ -14,6 +14,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\framework\Inspectron.Settings\Inspectron.Settings.csproj" /> <ProjectReference Include="..\framework\Inspectron.Settings\Inspectron.Settings.csproj" />
<ProjectReference Include="..\framework\Inspectron.Settings.Avalonia\Inspectron.Settings.Avalonia.csproj" />
<ProjectReference Include="..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj" /> <ProjectReference Include="..\VisionBuilder.UI.Common\VisionBuilder.UI.Common.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -4,6 +4,7 @@ using MudBlazor.Services;
using Ninject; using Ninject;
using Ninject.Extensions.ChildKernel; using Ninject.Extensions.ChildKernel;
using VisionBuilder.UI.Blazor; using VisionBuilder.UI.Blazor;
using VisionBuilder.UI.Camera;
using VisionBuilder.UI.Common; using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Plugins; using VisionBuilder.UI.Common.Plugins;
using VisionBuilder.UI.Common.Processing; using VisionBuilder.UI.Common.Processing;

View File

@@ -7,7 +7,7 @@ using Ninject.Extensions.ChildKernel;
using VisionBuilder.UI.Common; using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Processing; using VisionBuilder.UI.Common.Processing;
namespace Lindt.Colorballs.Duo.Utils; namespace VisionBuilder.UI.Camera;
public static class ModuleExtensions public static class ModuleExtensions
{ {
@@ -17,6 +17,7 @@ public static class ModuleExtensions
self.Bind<EmulationSettings, ISettings>().ToConstant(new EmulationSettings(cameraName)); self.Bind<EmulationSettings, ISettings>().ToConstant(new EmulationSettings(cameraName));
self.Bind<HawkeyeSettings, ISettings>().ToConstant(new HawkeyeSettings(cameraName)); self.Bind<HawkeyeSettings, ISettings>().ToConstant(new HawkeyeSettings(cameraName));
self.Bind<IDSImageSourceSettings, ISettings>().ToConstant(new IDSImageSourceSettings(cameraName)); self.Bind<IDSImageSourceSettings, ISettings>().ToConstant(new IDSImageSourceSettings(cameraName));
self.Bind<LibcameraCaptureSettings, ISettings>().ToConstant(new LibcameraCaptureSettings(cameraName));
self.Bind<SingleCameraVM>().ToSelf().InSingletonScope(); self.Bind<SingleCameraVM>().ToSelf().InSingletonScope();
return self; return self;
} }

View File

@@ -2,6 +2,7 @@
using Lindt.Colorballs.Duo.Utils; using Lindt.Colorballs.Duo.Utils;
using Ninject; using Ninject;
using Ninject.Extensions.ChildKernel; using Ninject.Extensions.ChildKernel;
using VisionBuilder.UI.Camera;
using VisionBuilder.UI.Common; using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Processing; using VisionBuilder.UI.Common.Processing;
using VisionBuilder.UI.Common.ViewModel; using VisionBuilder.UI.Common.ViewModel;

View File

@@ -8,6 +8,7 @@ using OpenCvSharp;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Threading; using System.Threading;
using VisionBuilder.UI.Camera;
using VisionBuilder.UI.Common; using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.Commands; using VisionBuilder.UI.Common.Commands;
using VisionBuilder.UI.Common.Plugins; using VisionBuilder.UI.Common.Plugins;

View File

@@ -119,6 +119,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LindtLeerformPlugin", "Plug
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Avalonia", "VisionBuilder.UI.Avalonia\VisionBuilder.UI.Avalonia.csproj", "{0549063E-5B22-4332-AD54-0D799B6DB4D2}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VisionBuilder.UI.Avalonia", "VisionBuilder.UI.Avalonia\VisionBuilder.UI.Avalonia.csproj", "{0549063E-5B22-4332-AD54-0D799B6DB4D2}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Inspectron.Settings.Avalonia", "framework\Inspectron.Settings.Avalonia\Inspectron.Settings.Avalonia.csproj", "{85063197-582A-4335-8298-431CF5CB95E1}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -643,6 +645,18 @@ Global
{0549063E-5B22-4332-AD54-0D799B6DB4D2}.Release|x64.Build.0 = Release|Any CPU {0549063E-5B22-4332-AD54-0D799B6DB4D2}.Release|x64.Build.0 = Release|Any CPU
{0549063E-5B22-4332-AD54-0D799B6DB4D2}.Release|x86.ActiveCfg = Release|Any CPU {0549063E-5B22-4332-AD54-0D799B6DB4D2}.Release|x86.ActiveCfg = Release|Any CPU
{0549063E-5B22-4332-AD54-0D799B6DB4D2}.Release|x86.Build.0 = Release|Any CPU {0549063E-5B22-4332-AD54-0D799B6DB4D2}.Release|x86.Build.0 = Release|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Debug|x64.ActiveCfg = Debug|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Debug|x64.Build.0 = Debug|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Debug|x86.ActiveCfg = Debug|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Debug|x86.Build.0 = Debug|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Release|Any CPU.Build.0 = Release|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Release|x64.ActiveCfg = Release|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Release|x64.Build.0 = Release|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Release|x86.ActiveCfg = Release|Any CPU
{85063197-582A-4335-8298-431CF5CB95E1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -685,6 +699,7 @@ Global
{149E2AFC-A714-4645-883A-2EE685E63FB9} = {F3414823-B70E-435E-B4EA-80ABF4371449} {149E2AFC-A714-4645-883A-2EE685E63FB9} = {F3414823-B70E-435E-B4EA-80ABF4371449}
{F70920C6-EF98-42B7-9F21-6E94781E9900} = {F3414823-B70E-435E-B4EA-80ABF4371449} {F70920C6-EF98-42B7-9F21-6E94781E9900} = {F3414823-B70E-435E-B4EA-80ABF4371449}
{5A1D9E37-353C-4E30-A302-48487DC3A9E8} = {583A77DF-A293-4F3E-AB96-7310BC495822} {5A1D9E37-353C-4E30-A302-48487DC3A9E8} = {583A77DF-A293-4F3E-AB96-7310BC495822}
{85063197-582A-4335-8298-431CF5CB95E1} = {2B8C63F7-B7FD-464C-9045-180ED8F595F1}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3CE42AE5-D79F-4E97-A246-AA8FD228B677} SolutionGuid = {3CE42AE5-D79F-4E97-A246-AA8FD228B677}

View File

@@ -0,0 +1,320 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform.Storage;
using Inspectron.Settings.Attributes;
namespace Inspectron.Settings.Avalonia.Configuration;
public class DefaultControlFactory : IControlFactory
{
public virtual Control CreateControl(string name, string description, PropertyInfo propertyInfo,
object initialValue, Action<object> valueChangedCallback, OptionsWindow optionsWindow)
{
var type = propertyInfo.PropertyType;
var settingDescriptionAttribute = propertyInfo.GetCustomAttribute<SettingDescriptionAttribute>();
string? settingDescription = settingDescriptionAttribute?.Description;
var previewAttribute = propertyInfo.GetCustomAttribute<SettingPreviewAttribute>();
TextBlock? previewLabel = null;
Func<object, string>? getPreview = null;
if (previewAttribute != null)
{
var method = previewAttribute.PreviewClass.GetMethod(previewAttribute.PreviewFunction, BindingFlags.Public | BindingFlags.Static);
if (method != null)
{
getPreview = value => (string)method.Invoke(null, new[] { value })!;
previewLabel = new TextBlock
{
Width = 600,
Height = 30,
Padding = new Thickness(15, 0, 0, 0),
FontStyle = FontStyle.Italic,
FontSize = 13,
Text = getPreview(initialValue)
};
}
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
{
return CreateListControl(name, description, type, initialValue, valueChangedCallback, optionsWindow, settingDescription, previewLabel, getPreview);
}
else if (type == typeof(string) && propertyInfo.GetCustomAttribute<FileAttribute>() != null)
{
return CreateFileControl(name, description, initialValue, valueChangedCallback, optionsWindow, settingDescription, previewLabel, getPreview, propertyInfo.GetCustomAttribute<FileAttribute>()!.Filter);
}
else if (type == typeof(string) && name.EndsWith("Path", StringComparison.OrdinalIgnoreCase))
{
return CreatePathControl(name, description, initialValue, valueChangedCallback, optionsWindow, settingDescription, previewLabel, getPreview);
}
else if (type == typeof(string))
{
return CreateStringControl(description, initialValue, valueChangedCallback, settingDescription, previewLabel, getPreview);
}
else if (type == typeof(int))
{
return CreateIntControl(description, initialValue, valueChangedCallback, settingDescription, previewLabel, getPreview);
}
else if (type == typeof(bool))
{
return CreateBoolControl(description, initialValue, valueChangedCallback, settingDescription, previewLabel, getPreview);
}
else if (type.IsEnum)
{
return CreateEnumControl(description, type, initialValue, valueChangedCallback, settingDescription, previewLabel, getPreview);
}
else
{
return new TextBlock { Text = $"Unsupported type: {type.Name}" };
}
}
private Control CreateListControl(string name, string description, Type listType, object initialValue,
Action<object> valueChangedCallback, OptionsWindow optionsWindow, string? settingDescription,
TextBlock? previewLabel, Func<object, string>? getPreview)
{
var elementType = listType.GetGenericArguments()[0];
var list = (IList)initialValue ?? (IList)Activator.CreateInstance(listType)!;
var label = new TextBlock { Text = description, Padding = new Thickness(0, 5, 0, 0) };
var textBox = new TextBox
{
Text = GetListDisplayText(list),
Width = 500,
Height = 60,
IsReadOnly = true,
AcceptsReturn = true,
TextWrapping = TextWrapping.Wrap
};
var editButton = new Button { Content = "Edit" };
editButton.Click += async (s, e) =>
{
var listEditor = new ListEditorDialog(elementType, list, this, optionsWindow, name);
var result = await listEditor.ShowDialog<IList?>(optionsWindow);
if (result != null)
{
list.Clear();
foreach (object o in result)
{
list.Add(o);
}
textBox.Text = GetListDisplayText(list);
valueChangedCallback(list);
if (previewLabel != null && getPreview != null)
previewLabel.Text = getPreview(list);
}
};
return CreateOuterPanel(settingDescription, previewLabel, label, textBox, editButton);
}
private 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 Control CreatePathControl(string name, string description, object initialValue, Action<object> valueChangedCallback,
OptionsWindow optionsWindow, string? settingDescription, TextBlock? previewLabel, Func<object, string>? getPreview)
{
var textBox = new TextBox { Text = initialValue as string, Width = 400 };
var label = new TextBlock { Text = description, Padding = new Thickness(0, 5, 0, 0) };
var browseButton = new Button { Content = "Browse" };
browseButton.Click += async (s, e) =>
{
var storageProvider = optionsWindow.StorageProvider;
var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
AllowMultiple = false,
Title = "Select Folder"
});
if (result.Count > 0)
{
var path = result[0].Path.LocalPath;
textBox.Text = path;
valueChangedCallback(path);
if (previewLabel != null && getPreview != null)
previewLabel.Text = getPreview(path);
}
};
textBox.TextChanged += (s, e) =>
{
valueChangedCallback(textBox.Text ?? "");
if (previewLabel != null && getPreview != null)
previewLabel.Text = getPreview(textBox.Text ?? "");
};
var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
row.Children.Add(textBox);
row.Children.Add(browseButton);
return CreateOuterPanel(settingDescription, previewLabel, label, row);
}
private Control CreateFileControl(string name, string description, object initialValue, Action<object> valueChangedCallback,
OptionsWindow optionsWindow, string? settingDescription, TextBlock? previewLabel, Func<object, string>? getPreview, string filter)
{
var textBox = new TextBox { Text = initialValue as string, Width = 400 };
var label = new TextBlock { Text = description, Padding = new Thickness(0, 5, 0, 0) };
var browseButton = new Button { Content = "Browse" };
browseButton.Click += async (s, e) =>
{
var storageProvider = optionsWindow.StorageProvider;
var result = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
AllowMultiple = false,
Title = "Select File",
FileTypeFilter = new[] { new FilePickerFileType(filter) { Patterns = new[] { filter } } }
});
if (result.Count > 0)
{
var path = result[0].Path.LocalPath;
textBox.Text = path;
valueChangedCallback(path);
if (previewLabel != null && getPreview != null)
previewLabel.Text = getPreview(path);
}
};
textBox.TextChanged += (s, e) =>
{
valueChangedCallback(textBox.Text ?? "");
if (previewLabel != null && getPreview != null)
previewLabel.Text = getPreview(textBox.Text ?? "");
};
var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
row.Children.Add(textBox);
row.Children.Add(browseButton);
return CreateOuterPanel(settingDescription, previewLabel, label, row);
}
private Control CreateStringControl(string description, object initialValue, Action<object> valueChangedCallback,
string? settingDescription, TextBlock? previewLabel, Func<object, string>? getPreview)
{
var textBox = new TextBox { Text = initialValue as string, Width = 600 };
var label = new TextBlock { Text = description, Padding = new Thickness(0, 5, 0, 0) };
textBox.TextChanged += (s, e) =>
{
valueChangedCallback(textBox.Text ?? "");
if (previewLabel != null && getPreview != null)
previewLabel.Text = getPreview(textBox.Text ?? "");
};
return CreateOuterPanel(settingDescription, previewLabel, label, textBox);
}
private Control CreateIntControl(string description, object initialValue, Action<object> valueChangedCallback,
string? settingDescription, TextBlock? previewLabel, Func<object, string>? getPreview)
{
var label = new TextBlock { Text = description, Padding = new Thickness(0, 5, 0, 0) };
var numericUpDown = new NumericUpDown
{
Minimum = 0,
Maximum = 99999999999,
Value = Convert.ToDecimal(initialValue),
Width = 200,
FormatString = "0"
};
numericUpDown.ValueChanged += (s, e) =>
{
valueChangedCallback(Convert.ToInt32(numericUpDown.Value));
if (previewLabel != null && getPreview != null)
previewLabel.Text = getPreview(Convert.ToInt32(numericUpDown.Value));
};
return CreateOuterPanel(settingDescription, previewLabel, label, numericUpDown);
}
private Control CreateBoolControl(string description, object initialValue, Action<object> valueChangedCallback,
string? settingDescription, TextBlock? previewLabel, Func<object, string>? getPreview)
{
var checkBox = new CheckBox { Content = description, IsChecked = (bool)initialValue };
checkBox.IsCheckedChanged += (s, e) =>
{
var isChecked = checkBox.IsChecked == true;
valueChangedCallback(isChecked);
if (previewLabel != null && getPreview != null)
previewLabel.Text = getPreview(isChecked);
};
return CreateOuterPanel(settingDescription, previewLabel, checkBox);
}
private Control CreateEnumControl(string description, Type enumType, object initialValue, Action<object> valueChangedCallback,
string? settingDescription, TextBlock? previewLabel, Func<object, string>? getPreview)
{
var label = new TextBlock { Text = description, Padding = new Thickness(0, 5, 0, 0) };
var comboBox = new ComboBox { Width = 200 };
var enumValues = Enum.GetValues(enumType);
foreach (var value in enumValues)
{
comboBox.Items.Add(value);
}
comboBox.SelectedItem = initialValue;
comboBox.SelectionChanged += (s, e) =>
{
if (comboBox.SelectedItem != null)
{
valueChangedCallback(comboBox.SelectedItem);
if (previewLabel != null && getPreview != null)
previewLabel.Text = getPreview(comboBox.SelectedItem);
}
};
return CreateOuterPanel(settingDescription, previewLabel, label, comboBox);
}
private Control CreateOuterPanel(string? settingDescription, TextBlock? previewLabel, params Control[] controls)
{
var outerPanel = new StackPanel { Orientation = Orientation.Vertical, Spacing = 4, Margin = new Thickness(0, 25, 0, 0), HorizontalAlignment = HorizontalAlignment.Left };
if (!string.IsNullOrEmpty(settingDescription))
{
var descriptionLabel = new TextBlock
{
Text = settingDescription,
FontStyle = FontStyle.Italic,
Padding = new Thickness(0, 5, 0, 0)
};
outerPanel.Children.Add(descriptionLabel);
}
foreach (var control in controls)
{
outerPanel.Children.Add(control);
}
if (previewLabel != null)
outerPanel.Children.Add(previewLabel);
return outerPanel;
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Reflection;
using Avalonia.Controls;
namespace Inspectron.Settings.Avalonia.Configuration;
public interface IControlFactory
{
Control CreateControl(string name, string description, PropertyInfo propertyInfo, object initialValue,
Action<object> valueChangedCallback, OptionsWindow optionsWindow);
}

View File

@@ -0,0 +1,29 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Inspectron.Settings.Avalonia.Configuration.ItemEditorDialog"
Title="Edit Item"
Width="650" Height="420"
WindowStartupLocation="CenterOwner"
CanResize="False">
<Grid RowDefinitions="*,Auto" Margin="12">
<!-- Properties panel -->
<ScrollViewer Grid.Row="0"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="PropertyPanel"
Orientation="Vertical"
Spacing="0" />
</ScrollViewer>
<!-- Bottom: OK/Cancel -->
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="8"
Margin="0,12,0,0">
<Button x:Name="BtnOK" Content="OK" Width="75" HorizontalContentAlignment="Center" />
<Button x:Name="BtnCancel" Content="Cancel" Width="75" HorizontalContentAlignment="Center" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,129 @@
using System;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace Inspectron.Settings.Avalonia.Configuration;
public partial class ItemEditorDialog : Window
{
private readonly Type _itemType;
private object? _workingValue;
private readonly DefaultControlFactory _controlFactory;
private readonly OptionsWindow _parentOptionsWindow;
public ItemEditorDialog(Type itemType, object? initialValue, DefaultControlFactory controlFactory, OptionsWindow parentOptionsWindow)
{
_itemType = itemType;
_workingValue = CloneObject(initialValue);
_controlFactory = controlFactory;
_parentOptionsWindow = parentOptionsWindow;
InitializeComponent();
Title = $"Edit {_itemType.Name}";
CreatePropertyControls();
BtnOK.Click += BtnOK_Click;
BtnCancel.Click += BtnCancel_Click;
}
private void CreatePropertyControls()
{
if (_itemType.IsValueType || _itemType == typeof(string))
{
CreateSimpleValueControl();
}
else
{
CreateComplexObjectControls();
}
}
private void CreateSimpleValueControl()
{
var dummyProperty = new SimplePropertyInfo(_itemType, "Value");
var control = _controlFactory.CreateControl("Value", "Value", dummyProperty, _workingValue!,
newValue => _workingValue = newValue, _parentOptionsWindow);
PropertyPanel.Children.Add(control);
}
private void CreateComplexObjectControls()
{
var properties = _itemType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(p => p.CanRead && p.CanWrite)
.ToArray();
foreach (var property in properties)
{
var currentValue = property.GetValue(_workingValue);
var control = _controlFactory.CreateControl(property.Name, property.Name, property, currentValue!,
newValue => property.SetValue(_workingValue, newValue), _parentOptionsWindow);
PropertyPanel.Children.Add(control);
}
}
private void BtnOK_Click(object? sender, RoutedEventArgs e)
{
Close(_workingValue);
}
private void BtnCancel_Click(object? sender, RoutedEventArgs e)
{
Close(null);
}
private object? CloneObject(object? obj)
{
if (obj == null) return GetDefaultValue(_itemType);
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 GetDefaultValue(type);
}
}
private object? GetDefaultValue(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
else if (type == typeof(string))
{
return "";
}
else
{
try
{
return Activator.CreateInstance(type);
}
catch
{
return null;
}
}
}
}

View File

@@ -0,0 +1,40 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Inspectron.Settings.Avalonia.Configuration.ListEditorDialog"
Title="Edit List"
Width="600" Height="400"
WindowStartupLocation="CenterOwner"
CanResize="False">
<Grid RowDefinitions="*,Auto" Margin="12">
<!-- Main content -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<!-- List -->
<ListBox x:Name="ItemListBox"
Grid.Column="0"
Margin="0,0,8,0"
SelectionMode="Single" />
<!-- Action buttons -->
<StackPanel Grid.Column="1"
Orientation="Vertical"
Spacing="8"
Width="75"
VerticalAlignment="Top">
<Button x:Name="BtnAdd" Content="Add" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" />
<Button x:Name="BtnEdit" Content="Edit" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" IsEnabled="False" />
<Button x:Name="BtnRemove" Content="Remove" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" IsEnabled="False" />
</StackPanel>
</Grid>
<!-- Bottom: OK/Cancel -->
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="8"
Margin="0,12,0,0">
<Button x:Name="BtnOK" Content="OK" Width="75" HorizontalContentAlignment="Center" />
<Button x:Name="BtnCancel" Content="Cancel" Width="75" HorizontalContentAlignment="Center" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,165 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace Inspectron.Settings.Avalonia.Configuration;
public partial class ListEditorDialog : Window
{
private readonly Type _elementType;
private readonly IList _workingList;
private readonly DefaultControlFactory _controlFactory;
private readonly OptionsWindow _parentOptionsWindow;
private readonly string _name;
public ListEditorDialog(Type elementType, IList originalList, DefaultControlFactory controlFactory, OptionsWindow parentOptionsWindow, string name)
{
_elementType = elementType;
_controlFactory = controlFactory;
_parentOptionsWindow = parentOptionsWindow;
_name = name;
var listType = typeof(List<>).MakeGenericType(elementType);
_workingList = (IList)Activator.CreateInstance(listType)!;
foreach (var item in originalList)
{
_workingList.Add(CloneObject(item));
}
InitializeComponent();
Title = $"Edit {_name}";
LoadListItems();
ItemListBox.SelectionChanged += ItemListBox_SelectionChanged;
BtnAdd.Click += BtnAdd_Click;
BtnEdit.Click += BtnEdit_Click;
BtnRemove.Click += BtnRemove_Click;
BtnOK.Click += BtnOK_Click;
BtnCancel.Click += BtnCancel_Click;
}
private void LoadListItems()
{
ItemListBox.Items.Clear();
foreach (var item in _workingList)
{
ItemListBox.Items.Add(item?.ToString() ?? "(null)");
}
}
private void ItemListBox_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
bool hasSelection = ItemListBox.SelectedIndex >= 0;
BtnEdit.IsEnabled = hasSelection;
BtnRemove.IsEnabled = hasSelection;
}
private async void BtnAdd_Click(object? sender, RoutedEventArgs e)
{
var defaultValue = GetDefaultValue(_elementType);
var itemEditor = new ItemEditorDialog(_elementType, defaultValue, _controlFactory, _parentOptionsWindow);
var result = await itemEditor.ShowDialog<object?>(this);
if (result != null)
{
_workingList.Add(result);
LoadListItems();
ItemListBox.SelectedIndex = ItemListBox.Items.Count - 1;
}
}
private async void BtnEdit_Click(object? sender, RoutedEventArgs e)
{
if (ItemListBox.SelectedIndex >= 0)
{
var selectedItem = _workingList[ItemListBox.SelectedIndex];
var itemEditor = new ItemEditorDialog(_elementType, selectedItem, _controlFactory, _parentOptionsWindow);
var result = await itemEditor.ShowDialog<object?>(this);
if (result != null)
{
_workingList[ItemListBox.SelectedIndex] = result;
LoadListItems();
}
}
}
private void BtnRemove_Click(object? sender, RoutedEventArgs e)
{
if (ItemListBox.SelectedIndex >= 0)
{
int selectedIndex = ItemListBox.SelectedIndex;
_workingList.RemoveAt(selectedIndex);
LoadListItems();
if (ItemListBox.Items.Count > 0)
{
ItemListBox.SelectedIndex = Math.Min(selectedIndex, ItemListBox.Items.Count - 1);
}
}
}
private void BtnOK_Click(object? sender, RoutedEventArgs e)
{
Close(_workingList);
}
private void BtnCancel_Click(object? sender, RoutedEventArgs e)
{
Close(null);
}
private object? GetDefaultValue(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
else if (type == typeof(string))
{
return "";
}
else
{
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

@@ -0,0 +1,12 @@
using System.Reflection;
namespace Inspectron.Settings.Avalonia.Configuration;
internal class OptionSetting
{
public object Owner { get; set; }
public PropertyInfo Property { get; set; }
public string Description { get; set; }
public string TreePath { get; set; }
public object Value { get; set; } // Temporary value until OK is pressed
}

View File

@@ -0,0 +1,44 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Inspectron.Settings.Avalonia.Configuration.OptionsWindow"
Title="Options"
Width="1089" Height="649"
WindowStartupLocation="CenterOwner"
CanResize="True">
<Grid RowDefinitions="*,Auto" Margin="12">
<!-- Main content -->
<Grid Grid.Row="0" ColumnDefinitions="210,12,*">
<!-- Left: Category tree -->
<TreeView x:Name="TreeViewCategories"
Grid.Column="0"
SelectionMode="Single" />
<!-- Right: Settings panel -->
<ScrollViewer Grid.Column="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel x:Name="SettingsPanel"
Orientation="Vertical"
Spacing="0"
HorizontalAlignment="Left" />
</ScrollViewer>
</Grid>
<!-- Bottom: OK/Cancel buttons -->
<StackPanel Grid.Row="1"
Orientation="Horizontal"
HorizontalAlignment="Right"
Spacing="8"
Margin="0,12,0,0">
<Button x:Name="BtnOK"
Content="OK"
Width="75" Height="30"
HorizontalContentAlignment="Center" />
<Button x:Name="BtnCancel"
Content="Cancel"
Width="75" Height="30"
HorizontalContentAlignment="Center" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,166 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Interactivity;
namespace Inspectron.Settings.Avalonia.Configuration;
public partial class OptionsWindow : Window
{
private readonly Dictionary<string, List<OptionSetting>> _settingsByCategory;
private readonly IControlFactory _controlFactory;
public OptionsWindow(IControlFactory controlFactory)
{
_controlFactory = controlFactory;
_settingsByCategory = new Dictionary<string, List<OptionSetting>>();
InitializeComponent();
TreeViewCategories.SelectionChanged += TreeViewCategories_SelectionChanged;
BtnOK.Click += BtnOK_Click;
BtnCancel.Click += BtnCancel_Click;
}
public void LoadFromSettings(InspectronSettings inspectronSettings)
{
void Traverse(object nodeObj, string path)
{
var node = nodeObj as dynamic;
object value = node.Value;
if (value is string folderName)
{
string newPath = string.IsNullOrEmpty(path) ? folderName : $"{path}/{folderName}";
foreach (var child in node.Children)
Traverse(child, newPath);
}
else if (value is InspectronSettings.UserSettingsInfo info)
{
string groupName = info.Name;
string groupPath = string.IsNullOrEmpty(path) ? groupName : $"{path}/{groupName}";
foreach (var pd in info.Settings)
{
object owner = null;
string description = pd.Description;
if (pd is BoundPropertyDescriptor bpd)
{
owner = bpd.Owner;
if (string.IsNullOrEmpty(description))
description = bpd.DisplayName;
}
else
{
continue;
}
RegisterUserSetting(owner, bpd.PropertyInfo.Name, groupPath, description);
}
}
}
Traverse(inspectronSettings.UserSettings.Root, "");
}
public void RegisterUserSetting(object owner, string propertyName, string treePath, string? description = null)
{
var property = owner.GetType().GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance);
if (property == null)
throw new ArgumentException($"Property '{propertyName}' not found on owner type.");
if (string.IsNullOrEmpty(description))
description = propertyName;
var setting = new OptionSetting
{
Owner = owner,
Property = property,
Description = description,
TreePath = treePath,
Value = property.GetValue(owner)
};
if (!_settingsByCategory.TryGetValue(treePath, out var settingsList))
{
settingsList = new List<OptionSetting>();
_settingsByCategory[treePath] = settingsList;
}
settingsList.Add(setting);
AddTreeNodes(treePath);
}
private void AddTreeNodes(string treePath)
{
var parts = treePath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
var items = TreeViewCategories.Items;
string currentPath = "";
foreach (var part in parts)
{
currentPath = string.IsNullOrEmpty(currentPath) ? part : $"{currentPath}/{part}";
var existing = items.OfType<TreeViewItem>().FirstOrDefault(n => (string)n.Header == part);
if (existing == null)
{
existing = new TreeViewItem { Header = part, Tag = currentPath, IsExpanded = false };
items.Add(existing);
}
items = existing.Items;
}
}
private void TreeViewCategories_SelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (TreeViewCategories.SelectedItem is TreeViewItem selectedItem)
{
var path = selectedItem.Tag as string;
if (path != null)
DisplaySettings(path);
}
}
private void DisplaySettings(string treePath)
{
SettingsPanel.Children.Clear();
if (_settingsByCategory.TryGetValue(treePath, out var settingsList))
{
foreach (var setting in settingsList)
{
var control = _controlFactory.CreateControl(
setting.Property.Name,
setting.Description,
setting.Property,
setting.Value,
(newValue) =>
{
setting.Value = newValue;
},
this);
SettingsPanel.Children.Add(control);
}
}
}
private void BtnOK_Click(object? sender, RoutedEventArgs e)
{
foreach (var settingsList in _settingsByCategory.Values)
{
foreach (var setting in settingsList)
{
setting.Property.SetValue(setting.Owner, setting.Value);
}
}
Close(true);
}
private void BtnCancel_Click(object? sender, RoutedEventArgs e)
{
Close(false);
}
}

View File

@@ -0,0 +1,34 @@
using System;
using System.Reflection;
namespace Inspectron.Settings.Avalonia.Configuration;
internal class SimplePropertyInfo : PropertyInfo
{
private readonly Type _propertyType;
private readonly string _name;
public SimplePropertyInfo(Type propertyType, string name)
{
_propertyType = propertyType;
_name = name;
}
public override PropertyAttributes Attributes => PropertyAttributes.None;
public override bool CanRead => true;
public override bool CanWrite => true;
public override Type PropertyType => _propertyType;
public override string Name => _name;
public override Type DeclaringType => typeof(object);
public override Type ReflectedType => typeof(object);
public override MethodInfo[] GetAccessors(bool nonPublic) => new MethodInfo[0];
public override MethodInfo GetGetMethod(bool nonPublic) => null;
public override ParameterInfo[] GetIndexParameters() => new ParameterInfo[0];
public override MethodInfo GetSetMethod(bool nonPublic) => null;
public override object GetValue(object obj, BindingFlags invokeAttr, Binder binder, object[] index, System.Globalization.CultureInfo culture) => obj;
public override void SetValue(object obj, object value, BindingFlags invokeAttr, Binder binder, object[] index, System.Globalization.CultureInfo culture) { }
public override object[] GetCustomAttributes(Type attributeType, bool inherit) => new object[0];
public override object[] GetCustomAttributes(bool inherit) => new object[0];
public override bool IsDefined(Type attributeType, bool inherit) => false;
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Inspectron.Settings\Inspectron.Settings.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,18 @@
using System.Linq;
using System.Reflection;
namespace Inspectron.Settings.Avalonia
{
public static class PropHelper
{
public static PropertyInfo GetPrp(object obj, string name)
{
return obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance)
.First(x => x.Name == name);
}
public static string SplitCamelCase(string input)
{
return System.Text.RegularExpressions.Regex.Replace(input, "([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim();
}
}
}