Merge branch 'feature/virtual-properties' into 'dev'

Resolve "Fully virtual properties"

Closes #24

See merge request leon.hoppe/hopframe!32
This commit was merged in pull request #70.
This commit is contained in:
2025-02-15 11:09:20 +00:00
11 changed files with 129 additions and 59 deletions

View File

@@ -12,10 +12,17 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment=""> <list default="true" id="0648788e-7696-4e60-bf12-5d5601f33d8c" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/docs/Dockerfile" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HopFrame/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/Writerside/hopframe.tree" beforeDir="false" afterPath="$PROJECT_DIR$/docs/Writerside/hopframe.tree" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/Writerside/topics/starter.md" beforeDir="false" afterPath="$PROJECT_DIR$/docs/Writerside/topics/Overview.md" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/HopFrameConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/HopFrameConfig.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/HopFrameConfiguratorExtensions.cs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/PropertyConfig.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Config/TableConfig.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Config/TableConfig.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/ContextExplorer.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/TableManager.cs" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Core/Services/Implementations/TableManager.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" beforeDir="false" afterPath="$PROJECT_DIR$/src/HopFrame.Web/Components/Dialogs/HopFrameEditor.razor" afterDir="false" />
<change beforePath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" beforeDir="false" afterPath="$PROJECT_DIR$/testing/HopFrame.Testing/Program.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tests/HopFrame.Tests.Core/Config/TableConfiguratorTests.cs" beforeDir="false" afterPath="$PROJECT_DIR$/tests/HopFrame.Tests.Core/Config/TableConfiguratorTests.cs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tests/HopFrame.Tests.Core/Services/DisplayPropertyTests.cs" beforeDir="false" afterPath="$PROJECT_DIR$/tests/HopFrame.Tests.Core/Services/DisplayPropertyTests.cs" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -35,7 +42,7 @@
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_BRANCH_BY_REPOSITORY"> <option name="RECENT_BRANCH_BY_REPOSITORY">
<map> <map>
<entry key="$PROJECT_DIR$" value="feature/plugins" /> <entry key="$PROJECT_DIR$" value="dev" />
</map> </map>
</option> </option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
@@ -74,6 +81,7 @@
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/558c1d46e1e21d2e78ee2ab67a674f6927bf95355b2f245f35d74bb5ec0f92/CancellationTokenSource.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/558c1d46e1e21d2e78ee2ab67a674f6927bf95355b2f245f35d74bb5ec0f92/CancellationTokenSource.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/5a69b82eed595b731b82667db08722b69b82482e275cf32dfb219190e3dc49/CollectionEntry.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/5a69b82eed595b731b82667db08722b69b82482e275cf32dfb219190e3dc49/CollectionEntry.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/60e7b22380df80ef6fefe43138047f49ec6eff4b25c12b42ce3d6ed5aac/MethodInvokerCommon.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/60e7b22380df80ef6fefe43138047f49ec6eff4b25c12b42ce3d6ed5aac/MethodInvokerCommon.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6354a7b35d7821629924d3676acd7e67a6f7f94343e0e66ec439aa2bd6ed5/ThrowHelper.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/642391624bd5c30b3411a11434588aba4906207335166b784bf3a4325f6c7/NavigationEntry.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/642391624bd5c30b3411a11434588aba4906207335166b784bf3a4325f6c7/NavigationEntry.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6d1d64f05e7045295fa180276a8c2aef0302c9e96eb53b3431ab13db4579/FluentAppBarItem.razor.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6d1d64f05e7045295fa180276a8c2aef0302c9e96eb53b3431ab13db4579/FluentAppBarItem.razor.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6fe785cceb29ca2d1da78e157315815a7c4372b582a20a71c28b210f9d56e/IconsExtensions.cs" root0="SKIP_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/6fe785cceb29ca2d1da78e157315815a7c4372b582a20a71c28b210f9d56e/IconsExtensions.cs" root0="SKIP_HIGHLIGHTING" />
@@ -97,8 +105,6 @@
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/fd57398b7dc3a8ce7da2786f2c67289c3d974658a9e90d0c1e84db3d965fbf1/Console.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/fd57398b7dc3a8ce7da2786f2c67289c3d974658a9e90d0c1e84db3d965fbf1/Console.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ff37d54b3bf4d2756237fb789635831532603376e940f63d634b869d26d74c/Regular16.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$APPLICATION_CONFIG_DIR$/resharper-host/SourcesCache/ff37d54b3bf4d2756237fb789635831532603376e940f63d634b869d26d74c/Regular16.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="file://$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.cs" root0="FORCE_HIGHLIGHTING" /> <setting file="file://$PROJECT_DIR$/src/HopFrame.Core/Config/DbContextConfig.cs" root0="FORCE_HIGHLIGHTING" />
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Web/Components/Pages/HopFrameTablePage.razor" root0="SKIP_HIGHLIGHTING" root1="FORCE_HIGHLIGHTING" root2="FORCE_HIGHLIGHTING" />
<setting file="mock://C:/Users/leon/Documents/Projekte/HopFrame/src/HopFrame.Web/Plugins/Events/SearchEvent.cs" root0="SKIP_HIGHLIGHTING" />
</component> </component>
<component name="KubernetesApiPersistence">{}</component> <component name="KubernetesApiPersistence">{}</component>
<component name="KubernetesApiProvider">{ <component name="KubernetesApiProvider">{
@@ -117,28 +123,28 @@
<option name="hideEmptyMiddlePackages" value="true" /> <option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" /> <option name="showLibraryContents" value="true" />
</component> </component>
<component name="PropertiesComponent">{ <component name="PropertiesComponent"><![CDATA[{
&quot;keyToString&quot;: { "keyToString": {
&quot;.NET Launch Settings Profile.HopFrame.Testing.Api: https.executor&quot;: &quot;Run&quot;, ".NET Launch Settings Profile.HopFrame.Testing.Api: https.executor": "Run",
&quot;.NET Launch Settings Profile.HopFrame.Testing.executor&quot;: &quot;Run&quot;, ".NET Launch Settings Profile.HopFrame.Testing.executor": "Run",
&quot;.NET Launch Settings Profile.HopFrame.Testing: https.executor&quot;: &quot;Run&quot;, ".NET Launch Settings Profile.HopFrame.Testing: https.executor": "Run",
&quot;.NET Project.HopFrame.Testing.executor&quot;: &quot;Run&quot;, ".NET Project.HopFrame.Testing.executor": "Run",
&quot;72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor&quot;: &quot;Debug&quot;, "72b118b0-a6fc-4561-acdf-74f0b454dbb8.executor": "Debug",
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, "RunOnceActivity.ShowReadmeOnStart": "true",
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;, "RunOnceActivity.git.unshallow": "true",
&quot;b5f11219-dfc4-47a1-b02c-90ab603034fb.executor&quot;: &quot;Debug&quot;, "b5f11219-dfc4-47a1-b02c-90ab603034fb.executor": "Debug",
&quot;dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor&quot;: &quot;Debug&quot;, "dcdf1689-dc07-47e4-8824-2e60a4fbf301.executor": "Debug",
&quot;git-widget-placeholder&quot;: &quot;!31 on feature/docs&quot;, "git-widget-placeholder": "!32 on feature/virtual-properties",
&quot;list.type.of.created.stylesheet&quot;: &quot;CSS&quot;, "list.type.of.created.stylesheet": "CSS",
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, "node.js.detected.package.eslint": "true",
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;, "node.js.detected.package.tslint": "true",
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, "node.js.selected.package.eslint": "(autodetect)",
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;, "node.js.selected.package.tslint": "(autodetect)",
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, "nodejs_package_manager_path": "npm",
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.pluginManager&quot;, "settings.editor.selected.configurable": "preferences.pluginManager",
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot; "vue.rearranger.settings.migration": "true"
} }
}</component> }]]></component>
<component name="RunManager" selected=".NET Launch Settings Profile.HopFrame.Testing: https"> <component name="RunManager" selected=".NET Launch Settings Profile.HopFrame.Testing: https">
<configuration name="HopFrame.Testing: http" type="LaunchSettings" factoryName=".NET Launch Settings Profile"> <configuration name="HopFrame.Testing: http" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" /> <option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/testing/HopFrame.Testing/HopFrame.Testing.csproj" />
@@ -248,6 +254,7 @@
<workItem from="1739352479748" duration="3047000" /> <workItem from="1739352479748" duration="3047000" />
<workItem from="1739369355001" duration="1751000" /> <workItem from="1739369355001" duration="1751000" />
<workItem from="1739461452173" duration="5533000" /> <workItem from="1739461452173" duration="5533000" />
<workItem from="1739550750776" duration="3388000" />
</task> </task>
<task id="LOCAL-00001" summary="Added basic configuration"> <task id="LOCAL-00001" summary="Added basic configuration">
<option name="closed" value="true" /> <option name="closed" value="true" />

View File

@@ -26,7 +26,7 @@ public class DbContextConfig {
/// <summary> /// <summary>
/// A helper class for editing the <see cref="DbContextConfig"/> /// A helper class for editing the <see cref="DbContextConfig"/>
/// </summary> /// </summary>
public class DbContextConfigurator<TDbContext>(DbContextConfig config) { public sealed class DbContextConfigurator<TDbContext>(DbContextConfig config) {
/// <summary> /// <summary>
/// The Internal DbContext configuration that's modified by the helper functions /// The Internal DbContext configuration that's modified by the helper functions

View File

@@ -15,7 +15,7 @@ public class HopFrameConfig {
/// <summary> /// <summary>
/// A helper class for editing the <see cref="HopFrameConfig"/> /// A helper class for editing the <see cref="HopFrameConfig"/>
/// </summary> /// </summary>
public class HopFrameConfigurator(HopFrameConfig config, IServiceCollection collection = null!) { public sealed class HopFrameConfigurator(HopFrameConfig config, IServiceCollection collection = null!) {
/// <summary> /// <summary>
/// The Internal HopFrame configuration that's modified by the helper functions /// The Internal HopFrame configuration that's modified by the helper functions

View File

@@ -23,9 +23,37 @@ public class PropertyConfig(PropertyInfo info, TableConfig table, int nthPropert
public bool IsRelation { get; internal set; } public bool IsRelation { get; internal set; }
public bool IsRequired { get; internal set; } public bool IsRequired { get; internal set; }
public bool IsEnumerable { get; internal set; } public bool IsEnumerable { get; internal set; }
public bool IsListingProperty { get; set; } public bool IsVirtualProperty { get; set; }
public int Order { get; set; } = nthProperty; public int Order { get; set; } = nthProperty;
public int DisplayLength { get; set; } = 32; public int DisplayLength { get; set; } = 32;
public virtual object? GetValue(object? source, IServiceProvider provider) {
return Info.GetValue(source);
}
public virtual void SetValue(object? source, object? value, IServiceProvider provider) {
Info.SetValue(source, value);
}
}
public sealed class VirtualPropertyConfig(TableConfig table, int nthProperty) : PropertyConfig(GetDummyProperty(), table, nthProperty) {
public string? DummyProperty { get; set; } = null;
public Func<object, string, IServiceProvider, Task>? VirtualParser { get; set; }
public override object? GetValue(object? source, IServiceProvider provider) {
return Formatter!.Invoke(source!, provider).Result;
}
public override void SetValue(object? source, object? value, IServiceProvider provider) {
VirtualParser?.Invoke(source!, (string)value!, provider).Wait();
}
private static PropertyInfo GetDummyProperty() {
return typeof(VirtualPropertyConfig)
.GetProperties()
.First(prop => prop.Name == nameof(DummyProperty));
}
} }
/// <summary> /// <summary>
@@ -213,3 +241,28 @@ public class PropertyConfigurator<TProp>(PropertyConfig config) {
return this; return this;
} }
} }
public sealed class VirtualPropertyConfigurator<TModel>(VirtualPropertyConfig config) : PropertyConfigurator<string>(config) {
/// <summary>
/// Determines the function used for parsing the value provided in the editor dialog to the actual model value
/// </summary>
public VirtualPropertyConfigurator<TModel> SetVirtualParser(Action<TModel, string, IServiceProvider> parser) {
var cfg = InnerConfig as VirtualPropertyConfig;
cfg!.VirtualParser = (model, input, services) => {
parser.Invoke((TModel)model, input, services);
return Task.CompletedTask;
};
return this;
}
/// <inheritdoc cref="SetVirtualParser{TModel}(System.Action{TModel,string,System.IServiceProvider})"/>
public VirtualPropertyConfigurator<TModel> SetVirtualParser(Func<TModel, string, IServiceProvider, Task> parser) {
var cfg = InnerConfig as VirtualPropertyConfig;
cfg!.VirtualParser = (model, input, services) => parser.Invoke((TModel)model, input, services);
return this;
}
}

View File

@@ -52,7 +52,7 @@ public class TableConfig {
/// <summary> /// <summary>
/// A helper class for editing the <see cref="TableConfig"/> /// A helper class for editing the <see cref="TableConfig"/>
/// </summary> /// </summary>
public class TableConfigurator<TModel>(TableConfig config) { public sealed class TableConfigurator<TModel>(TableConfig config) {
/// <summary> /// <summary>
/// The Internal property configuration that's modified by the helper functions /// The Internal property configuration that's modified by the helper functions
@@ -76,7 +76,7 @@ public class TableConfigurator<TModel>(TableConfig config) {
public PropertyConfigurator<TProp> Property<TProp>(Expression<Func<TModel, TProp>> propertyExpression) { public PropertyConfigurator<TProp> Property<TProp>(Expression<Func<TModel, TProp>> propertyExpression) {
var info = GetPropertyInfo(propertyExpression); var info = GetPropertyInfo(propertyExpression);
var prop = InnerConfig.Properties var prop = InnerConfig.Properties
.Single(prop => prop.Info.Name == info.Name); .Single(prop => prop.Info == info);
return new PropertyConfigurator<TProp>(prop); return new PropertyConfigurator<TProp>(prop);
} }
@@ -99,25 +99,25 @@ public class TableConfigurator<TModel>(TableConfig config) {
/// <param name="template">The template used for generating the property value</param> /// <param name="template">The template used for generating the property value</param>
/// <returns>The configurator for the virtual property</returns> /// <returns>The configurator for the virtual property</returns>
/// <seealso cref="PropertyConfigurator{TProp}"/> /// <seealso cref="PropertyConfigurator{TProp}"/>
public PropertyConfigurator<string> AddVirtualProperty(string name, Func<TModel, IServiceProvider, string> template) { public VirtualPropertyConfigurator<TModel> AddVirtualProperty(string name, Func<TModel, IServiceProvider, string> template) {
var prop = new PropertyConfig(InnerConfig.Properties.First().Info, InnerConfig, InnerConfig.Properties.Count) { var prop = new VirtualPropertyConfig(InnerConfig, InnerConfig.Properties.Count) {
Name = name, Name = name,
IsListingProperty = true, IsVirtualProperty = true,
Formatter = (obj, provider) => Task.FromResult(template.Invoke((TModel)obj, provider)) Formatter = (obj, provider) => Task.FromResult(template.Invoke((TModel)obj, provider))
}; };
InnerConfig.Properties.Add(prop); InnerConfig.Properties.Add(prop);
return new PropertyConfigurator<string>(prop); return new VirtualPropertyConfigurator<TModel>(prop);
} }
/// <inheritdoc cref="AddVirtualProperty(string,System.Func{TModel,System.IServiceProvider,string})"/> /// <inheritdoc cref="AddVirtualProperty(string,System.Func{TModel,System.IServiceProvider,string})"/>
public PropertyConfigurator<string> AddVirtualProperty(string name, Func<TModel, IServiceProvider, Task<string>> template) { public VirtualPropertyConfigurator<TModel> AddVirtualProperty(string name, Func<TModel, IServiceProvider, Task<string>> template) {
var prop = new PropertyConfig(InnerConfig.Properties.First().Info, InnerConfig, InnerConfig.Properties.Count) { var prop = new VirtualPropertyConfig(InnerConfig, InnerConfig.Properties.Count) {
Name = name, Name = name,
IsListingProperty = true, IsVirtualProperty = true,
Formatter = (obj, provider) => template.Invoke((TModel)obj, provider) Formatter = (obj, provider) => template.Invoke((TModel)obj, provider)
}; };
InnerConfig.Properties.Add(prop); InnerConfig.Properties.Add(prop);
return new PropertyConfigurator<string>(prop); return new VirtualPropertyConfigurator<TModel>(prop);
} }
/// <summary> /// <summary>
@@ -229,7 +229,7 @@ public class TableConfigurator<TModel>(TableConfig config) {
throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property."); throw new ArgumentException($"Expression '{propertyLambda}' refers to a field, not a property.");
} }
Type type = typeof(TSource); var type = typeof(TSource);
if (propInfo.ReflectedType != null && type != propInfo.ReflectedType && if (propInfo.ReflectedType != null && type != propInfo.ReflectedType &&
!type.IsSubclassOf(propInfo.ReflectedType)) { !type.IsSubclassOf(propInfo.ReflectedType)) {
throw new ArgumentException($"Expression '{propertyLambda}' refers to a property that is not from type {type}."); throw new ArgumentException($"Expression '{propertyLambda}' refers to a property that is not from type {type}.");

View File

@@ -61,7 +61,7 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
var entity = dbContext.Model.FindEntityType(table.TableType)!; var entity = dbContext.Model.FindEntityType(table.TableType)!;
foreach (var propertyConfig in table.Properties) { foreach (var propertyConfig in table.Properties) {
if (propertyConfig.IsListingProperty) continue; if (propertyConfig.IsVirtualProperty) continue;
if (propertyConfig.IsRelation) continue; if (propertyConfig.IsRelation) continue;
var prop = entity.FindProperty(propertyConfig.Info.Name); var prop = entity.FindProperty(propertyConfig.Info.Name);
@@ -93,7 +93,7 @@ internal sealed class ContextExplorer(HopFrameConfig config, IServiceProvider pr
foreach (var property in entity.GetProperties()) { foreach (var property in entity.GetProperties()) {
var propConfig = table.Properties var propConfig = table.Properties
.Where(prop => !prop.IsListingProperty) .Where(prop => !prop.IsVirtualProperty)
.SingleOrDefault(prop => prop.Info == property.PropertyInfo); .SingleOrDefault(prop => prop.Info == property.PropertyInfo);
if (propConfig is null || propConfig.IsRequired) continue; if (propConfig is null || propConfig.IsRequired) continue;
propConfig.IsRequired = !property.IsNullable; propConfig.IsRequired = !property.IsNullable;

View File

@@ -63,7 +63,7 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
private bool ItemSearched(TModel item, string searchTerm) { private bool ItemSearched(TModel item, string searchTerm) {
foreach (var property in config.Properties) { foreach (var property in config.Properties) {
if (!property.Searchable) continue; if (!property.Searchable) continue;
var value = property.Info.GetValue(item); var value = property.GetValue(item, provider);
if (value is null) continue; if (value is null) continue;
var strValue = value.ToString(); var strValue = value.ToString();
@@ -77,10 +77,10 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
public async Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null) { public async Task<string> DisplayProperty(object? item, PropertyConfig prop, object? value = null, object? enumerableValue = null) {
if (item is null) return string.Empty; if (item is null) return string.Empty;
if (prop.IsListingProperty) if (prop.IsVirtualProperty)
return await prop.Formatter!.Invoke(item, provider); return await prop.Formatter!.Invoke(item, provider);
var propValue = value ?? prop.Info.GetValue(item); var propValue = value ?? prop.GetValue(item, provider);
if (propValue is null) if (propValue is null)
return string.Empty; return string.Empty;
@@ -112,7 +112,7 @@ internal sealed class TableManager<TModel>(DbContext context, TableConfig config
if (innerConfig is null) return propValue.ToString()!; if (innerConfig is null) return propValue.ToString()!;
var innerProp = innerConfig.Properties var innerProp = innerConfig.Properties
.SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsListingProperty); .SingleOrDefault(p => p.Info == prop.DisplayedProperty && !p.IsVirtualProperty);
if (innerProp is null) return propValue.ToString() ?? string.Empty; if (innerProp is null) return propValue.ToString() ?? string.Empty;
return await DisplayProperty(propValue, innerProp); return await DisplayProperty(propValue, innerProp);

View File

@@ -11,7 +11,7 @@
@using HopFrame.Web.Plugins.Events @using HopFrame.Web.Plugins.Events
<FluentDialogBody> <FluentDialogBody>
@foreach (var property in Content.Config.Properties.Where(prop => !prop.IsListingProperty).OrderBy(prop => prop.Order)) { @foreach (var property in GetEditorProperties()) {
if (!_currentlyEditing && !property.Creatable) continue; if (!_currentlyEditing && !property.Creatable) continue;
<div style="margin-bottom: 20px"> <div style="margin-bottom: 20px">
@@ -158,7 +158,7 @@
ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Text))" /> ValueChanged="@(async v => await SetPropertyValue(property, v, InputType.Text))" />
} }
@foreach (var error in _validationErrors[property.Info.Name]) { @foreach (var error in _validationErrors[property.Name]) {
<FluentLabel Color="@Color.Error">@error</FluentLabel> <FluentLabel Color="@Color.Error">@error</FluentLabel>
} }
</div> </div>
@@ -197,11 +197,16 @@
Content.CurrentObject ??= Activator.CreateInstance(Content.Config.TableType); Content.CurrentObject ??= Activator.CreateInstance(Content.Config.TableType);
foreach (var property in Content.Config.Properties) { foreach (var property in Content.Config.Properties) {
if (property.IsListingProperty) continue; _validationErrors.Add(property.Name, []);
_validationErrors.Add(property.Info.Name, []);
} }
} }
private IEnumerable<PropertyConfig> GetEditorProperties() {
return Content.Config.Properties
.Where(prop => prop is not VirtualPropertyConfig { VirtualParser: null })
.OrderBy(prop => prop.Order);
}
private TValue? GetPropertyValue<TValue>(PropertyConfig config, object? listItem = null) { private TValue? GetPropertyValue<TValue>(PropertyConfig config, object? listItem = null) {
if (!config.DisplayValue) return default; if (!config.DisplayValue) return default;
if (Content.CurrentObject is null) return default; if (Content.CurrentObject is null) return default;
@@ -315,12 +320,12 @@
private void ApplyChanges(object entry) { private void ApplyChanges(object entry) {
foreach (var prop in Content.Config.Properties) { foreach (var prop in Content.Config.Properties) {
var newValue = GetNewestValue(prop); var newValue = GetNewestValue(prop);
prop.Info.SetValue(entry, newValue); prop.SetValue(entry, newValue, Provider);
} }
} }
private object? GetNewestValue(PropertyConfig config) { private object? GetNewestValue(PropertyConfig config) {
var value = config.Info.GetValue(Content.CurrentObject); var value = config.GetValue(Content.CurrentObject, Provider);
var change = _changes.LastOrDefault(c => c.Property == config.Info); var change = _changes.LastOrDefault(c => c.Property == config.Info);
if (change is not null) if (change is not null)
@@ -366,9 +371,9 @@
return false; return false;
foreach (var property in Content.Config.Properties) { foreach (var property in Content.Config.Properties) {
if (property.IsListingProperty) continue; if (property.IsVirtualProperty) continue;
var errorList = _validationErrors[property.Info.Name]; var errorList = _validationErrors[property.Name];
errorList.Clear(); errorList.Clear();
var value = GetNewestValue(property); var value = GetNewestValue(property);

View File

@@ -34,6 +34,11 @@ builder.Services.AddHopFrame(options => {
.SetOrderIndex(3); .SetOrderIndex(3);
table.AddVirtualProperty("Name", (user, _) => $"{user.FirstName} {user.LastName}") table.AddVirtualProperty("Name", (user, _) => $"{user.FirstName} {user.LastName}")
/*.SetVirtualParser((model, input, _) => {
var split = input.Split(' ');
model.FirstName = split.FirstOrDefault();
model.LastName = split.LastOrDefault();
})*/
.SetOrderIndex(2); .SetOrderIndex(2);
table.SetDisplayName("Benutzer"); table.SetDisplayName("Benutzer");

View File

@@ -63,7 +63,7 @@ public class TableConfiguratorTests {
var virtualProperty = tableConfig.Properties.SingleOrDefault(p => p.Name == "VirtualName"); var virtualProperty = tableConfig.Properties.SingleOrDefault(p => p.Name == "VirtualName");
Assert.NotNull(virtualProperty); Assert.NotNull(virtualProperty);
Assert.NotNull(propertyConfigurator); Assert.NotNull(propertyConfigurator);
Assert.True(virtualProperty.IsListingProperty); Assert.True(virtualProperty.IsVirtualProperty);
Assert.Equal("VirtualName", virtualProperty.Name); Assert.Equal("VirtualName", virtualProperty.Name);
} }
@@ -84,7 +84,7 @@ public class TableConfiguratorTests {
var virtualProperty = tableConfig.Properties.SingleOrDefault(p => p.Name == "VirtualName"); var virtualProperty = tableConfig.Properties.SingleOrDefault(p => p.Name == "VirtualName");
Assert.NotNull(virtualProperty); Assert.NotNull(virtualProperty);
Assert.NotNull(propertyConfigurator); Assert.NotNull(propertyConfigurator);
Assert.True(virtualProperty.IsListingProperty); Assert.True(virtualProperty.IsVirtualProperty);
Assert.Equal("VirtualName", virtualProperty.Name); Assert.Equal("VirtualName", virtualProperty.Name);
} }

View File

@@ -39,7 +39,7 @@ public class DisplayPropertyTests {
// Arrange // Arrange
var item = "test"; var item = "test";
var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0) { var prop = new PropertyConfig(typeof(string).GetProperty("Length")!, _config, 0) {
IsListingProperty = true, IsVirtualProperty = true,
Formatter = (obj, provider) => Task.FromResult(((string)obj).ToUpper()) Formatter = (obj, provider) => Task.FromResult(((string)obj).ToUpper())
}; };