Skip to content

Commit

Permalink
Merge pull request #453 from TimeWarpEngineering/Cramer/2024-07-25/Next
Browse files Browse the repository at this point in the history
Add RegisterRenderTrigger that takes an Expression and caches complied version
  • Loading branch information
StevenTCramer authored Jul 26, 2024
2 parents 8a206f7 + 3a7fcdd commit 1973ca2
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project>
<!-- Set common properties regarding assembly information and nuget packages -->
<PropertyGroup>
<TimeWarpStateVersion>11.0.0-beta.68+8.0.303</TimeWarpStateVersion>
<TimeWarpStateVersion>11.0.0-beta.69+8.0.303</TimeWarpStateVersion>
<Authors>Steven T. Cramer</Authors>
<Product>TimeWarp State</Product>
<PackageVersion>$(TimeWarpStateVersion)</PackageVersion>
Expand Down
54 changes: 54 additions & 0 deletions Source/TimeWarp.State/Components/TimeWarpStateComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class TimeWarpStateComponent : ComponentBase, IDisposable, ITimeWarpState
[Parameter] public string? TestId { get; set; }

Check warning on line 33 in Source/TimeWarp.State/Components/TimeWarpStateComponent.cs

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Auto-property accessor is never used (non-private accessibility)

Auto-property accessor 'TestId.set' is never used

private readonly ConcurrentDictionary <Type, Func<bool>> RenderTriggers = new();
private readonly ConcurrentDictionary<(Type StateType, string PropertyName), Func<object, object, bool>> CompiledPropertyComparisons = new();

/// <summary>
/// Set this to true if something in the component has changed that requires a re-render.
Expand Down Expand Up @@ -176,6 +177,59 @@ protected void RegisterRenderTrigger<T>(Func<T, bool> triggerCondition) where T
RenderTriggers[typeof(T)] = () => ShouldReRender(typeof(T), triggerCondition);
}

/// <summary>
/// Registers a render trigger for a specific state type using a property selector expression.
/// </summary>
/// <typeparam name="TState">The type of state to monitor. Must be a reference type.</typeparam>
/// <param name="propertySelector">An expression that selects the property to monitor for changes.</param>
/// <remarks>
/// This method creates a render trigger that compares a specific property of the state object.
/// It uses expression trees to build an efficient comparison function, which is cached for subsequent use.
/// The component will re-render when the selected property's value changes.
/// </remarks>
/// <example>
/// <code>
/// RegisterRenderTrigger&lt;CounterState&gt;(s => s.Count);
/// </code>
/// </example>
/// <exception cref="ArgumentNullException">Thrown when the propertySelector is null.</exception>
/// <exception cref="ArgumentException">Thrown when the propertySelector does not represent a simple property access.</exception>
protected void RegisterRenderTrigger<TState>(Expression<Func<TState, object>> propertySelector)
where TState : class
{
ArgumentNullException.ThrowIfNull(propertySelector);

MemberExpression? memberExpression = propertySelector.Body as MemberExpression
?? ((UnaryExpression)propertySelector.Body).Operand as MemberExpression;

if (memberExpression == null)
{
throw new ArgumentException("Property selector must be a simple property access expression.", nameof(propertySelector));
}

var property = (PropertyInfo)memberExpression.Member;

Func<object, object, bool> comparisonFunc = CompiledPropertyComparisons.GetOrAdd((typeof(TState), property.Name), _ =>
{
ParameterExpression previousParam = Expression.Parameter(typeof(object), "previous");
ParameterExpression currentParam = Expression.Parameter(typeof(object), "current");
BinaryExpression notEqualExpression = Expression.NotEqual(
Expression.Property(Expression.Convert(previousParam, typeof(TState)), property),
Expression.Property(Expression.Convert(currentParam, typeof(TState)), property)
);
return Expression.Lambda<Func<object, object, bool>>(notEqualExpression, previousParam, currentParam).Compile();
});

RenderTriggers[typeof(TState)] = () =>
{
TState? previousState = GetPreviousState<TState>();
TState currentState = GetState<TState>(false);
return previousState == null || comparisonFunc(previousState, currentState);
};
}

/// <summary>
/// Place a Subscription for the calling component
/// And returns the requested state
Expand Down
2 changes: 1 addition & 1 deletion Source/TimeWarp.State/Components/TimeWarpStateComponent.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The `TimeWarpStateComponent` is a crucial base class in the TimeWarp.State libra
### 2. Instance Tracking

- Generates a unique `Id` for each component instance
- The primary purpose of the ID is for placing of subscriptions
- The `Id` serves as a unique key in the subscription system
- Also useful for debugging and component identification

### 3. Render Mode Management
Expand Down
1 change: 1 addition & 0 deletions Source/TimeWarp.State/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
global using System.Text.RegularExpressions;
global using System.Text;
global using System;
global using System.Linq.Expressions;
global using TimeWarp.Features.Developer;
global using TimeWarp.Features.JavaScriptInterop;
global using TimeWarp.Features.ReduxDevTools;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
@namespace Test.App.Client.Features.Counter.Components
@inherits BaseComponent

@code
{
protected override void OnInitialized()

Check warning on line 6 in Tests/Test.App/Test.App.Client/Features/Counter/Components/Counter.razor

View workflow job for this annotation

GitHub Actions / Qodana for .NET

Missing XML comment for publicly visible type or member

Missing XML comment for publicly visible type or member 'Test.App.Client.Features.Counter.Components.Counter.OnInitialized'
{
RegisterRenderTrigger<CounterState>(p => p.Count != CounterState.Count);
RegisterRenderTrigger<CounterState>(p => p.Count);
base.OnInitialized();
}
}

<div data-qa="@TestId">
<p>CounterState.Count: <span data-qa="count">@CounterState.Count</span></p>

Expand Down

0 comments on commit 1973ca2

Please sign in to comment.