Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PackageProjectUrl>https://www.github.com/JJConsulting/JJMasterData</PackageProjectUrl>
<RepositoryUrl>https://www.github.com/JJConsulting/JJMasterData</RepositoryUrl>
<PackageIcon>JJMasterData.png</PackageIcon>
<VersionPrefix>5.0.14</VersionPrefix>
<VersionPrefix>5.0.15</VersionPrefix>
<Version>$(VersionPrefix)</Version>
<Version Condition="'$(VersionSuffix)' != ''">$(VersionPrefix)-$(VersionSuffix)</Version>
<PackageVersion>$(Version)</PackageVersion>
Expand Down
1 change: 1 addition & 0 deletions doc/MasterData.Docs/articles/developer-guide/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Choose an extension according to the behavior being changed:
| --- | --- | --- |
| Validate or modify submitted values; react after persistence | `IFormEventHandler` | [Form events](form-events.md) |
| Customize grid filters, data and rendered content | `IGridEventHandler` | [Grid events](grid-events.md) |
| Choose how grid preferences are stored | `WithGridPreferenceStore<T>()` / `WithGridSessionPreferenceStore()` | [Grid Preference Store](grid-preference-store.md) |
| Add a downloadable file format and its options | `IExportFormat<TOptions>` | [Custom export formats](export-formats.md) |
| Add an expression provider | `WithExpressionProvider<T>()` | [Expressions](../concepts/expressions.md) |
| Add an action plugin | `WithActionPlugin<TPlugin>()` | [Actions](../concepts/actions.md) |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Grid Preference Store

`IGridPreferenceStore` reads and saves one `GridPreferences` object per grid in the store's current scope. It contains display settings, filters, sort order and the current page. Enable
`FormElement.Options.Grid.MaintainValuesOnLoad` ("Save User Preferences" in the dictionary editor) to save and restore
them.

The stores and interface are in `JJMasterData.Core.UI.Components`. Builder methods are in
`JJMasterData.Core.Configuration`.

Consume `IGridPreferenceStore` through constructor injection. The GridView and its factory receive the store directly.

| Store | Storage | Lifetime |
|---------------------------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `CookieGridPreferenceStore` | Protected browser cookies | All preferences use browser session cookies without an explicit expiration. |
| `SessionGridPreferenceStore` | ASP.NET Core session | All preferences last until the session expires or the host clears them. |
| Custom `IGridPreferenceStore` | Chosen by the host | Chosen by the implementation. |

By default, JJMasterData selects `SessionGridPreferenceStore` when the current request has a session provided by `UseSession()`. Otherwise, it uses `CookieGridPreferenceStore`. The choice is made when the scoped `IGridPreferenceStore` is resolved, so the order of `AddSession()` and `AddJJMasterDataWeb()` does not matter. `AddSession()` alone does not activate the session middleware.

An explicit `WithGridPreferenceStore<T>()`, factory registration or `WithGridSessionPreferenceStore()` overrides automatic selection.

The cookie store isolates preferences by `IMasterDataUser.Id`, application path and grid name. The session store uses the grid name within the current ASP.NET Core session. Both ignore grid-name casing and skip storage when the user ID is missing. `MasterDataCoreOptions.UserIdClaimType` controls the default user ID claim.

## Current session only

Configure the host's session services and middleware. JJMasterData will then use the session store automatically:

```csharp
using JJMasterData.Core.Configuration;
using JJMasterData.Web.Configuration;

builder.Services.AddDistributedMemoryCache(); // Or use the host's distributed cache.
builder.Services.AddSession();
builder.Services.AddJJMasterDataWeb(builder.Configuration);

var app = builder.Build();
app.UseRouting();
app.UseSession();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapDefaultControllerRoute();
```

To require session storage explicitly, use `AddJJMasterDataWeb(...).WithGridSessionPreferenceStore()`.
This requires `AddSession()` and `UseSession()` and does not fall back to cookies. To force cookies even when a session exists, use `.WithGridPreferenceStore<CookieGridPreferenceStore>()`.

ASP.NET Core session is independent of authentication. Signing out does not automatically clear it. To discard the
current session's data on logout, the host can call `HttpContext.Session.Clear()`; this clears all session entries,
including data unrelated to the grid.

## Custom store

Implement `IGridPreferenceStore`, then register it through the builder:

```csharp
builder.Services.AddJJMasterDataWeb(builder.Configuration)
.WithGridPreferenceStore<ApplicationGridPreferenceStore>();
```

A factory overload is also available:

```csharp
builder.Services.AddJJMasterDataWeb(builder.Configuration)
.WithGridPreferenceStore(services => new ApplicationGridPreferenceStore(
services.GetRequiredService<IHttpContextAccessor>()));
```

Both overloads replace the current store with a scoped registration and return the builder. The last store selection
wins. `WithGridSessionPreferenceStore()` is shorthand for `WithGridPreferenceStore<SessionGridPreferenceStore>()`.

Implement only two methods:

```csharp
public interface IGridPreferenceStore
{
GridPreferences? Get(string gridName);
void Set(string gridName, GridPreferences? preferences);
}
```
2 changes: 2 additions & 0 deletions doc/MasterData.Docs/articles/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@
href: developer-guide/form-events.md
- name: Grid events
href: developer-guide/grid-events.md
- name: Grid Preference Store
href: developer-guide/grid-preference-store.md
- name: Custom export formats
href: developer-guide/export-formats.md
- name: Authorization
Expand Down
1 change: 1 addition & 0 deletions doc/MasterData.Docs/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Dictionary metadata and business records use separate repositories. Saving metad
- [Routes and embedding](articles/developer-guide/routes-and-embedding.html): Embed components in controllers and Razor views; preserve callback responses.
- [Data panels and application-owned saves](articles/developer-guide/data-panels.html): Complete MVC controller/view example using ViewData, explicit validation and saving.
- [UI customization](articles/developer-guide/ui-customization.html): Runtime metadata changes and shared host assets.
- [Grid Preference Store](articles/developer-guide/grid-preference-store.html): Cookie, session and custom storage for grid preferences.
- [Form events](articles/developer-guide/form-events.html): Registration, insert/update/delete/import hooks and transaction boundaries.
- [Grid events](articles/developer-guide/grid-events.html): Custom data loading, pagination, rendering, selection and editable values.
- [Authorization](articles/developer-guide/authorization.html): Protect administration, runtime routes and record access.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using JJMasterData.Core.DataManager.Exportation.Abstractions;
using JJMasterData.Core.DataManager.Expressions.Abstractions;
using JJMasterData.Core.Events.Abstractions;
using JJMasterData.Core.UI.Components;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -21,6 +22,29 @@ public static class MasterDataServiceBuilderExtensions
{
extension(MasterDataServiceBuilder builder)
{
/// <summary>Replaces the scoped store used for GridView preferences.</summary>
public MasterDataServiceBuilder WithGridPreferenceStore<T>() where T : class, IGridPreferenceStore
{
builder.Services.Replace(ServiceDescriptor.Scoped<IGridPreferenceStore, T>());
return builder;
}

/// <summary>Replaces the scoped store used for GridView preferences.</summary>
public MasterDataServiceBuilder WithGridPreferenceStore(Func<IServiceProvider, IGridPreferenceStore> implementationFactory)
{
builder.Services.Replace(ServiceDescriptor.Scoped(implementationFactory));
return builder;
}

/// <summary>
/// Stores all grid preferences in the current ASP.NET Core session.
/// The host must configure AddSession and UseSession.
/// </summary>
public MasterDataServiceBuilder WithGridSessionPreferenceStore()
{
return builder.WithGridPreferenceStore<SessionGridPreferenceStore>();
}

public MasterDataServiceBuilder WithExportFormat<TFormat>() where TFormat : class, IExportFormat
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped<IExportFormat, TFormat>());
Expand Down
11 changes: 11 additions & 0 deletions src/MasterData.Core/Configuration/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
using JJMasterData.Core.DataManager.Exportation.Formats;
using JJMasterData.Core.DataManager.Importation.Background;
using JJMasterData.Core.Html.Templates;
using JJMasterData.Core.UI.Components;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
Expand Down Expand Up @@ -50,6 +52,15 @@ private void AddMasterDataCoreServices()
services.AddOptions<MasterDataCoreOptions>().BindConfiguration("JJMasterData");

services.AddHttpServices();
services.AddDataProtection();
services.TryAddScoped<IGridPreferenceStore>(provider =>
{
var context = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
if (context?.Features.Get<ISessionFeature>()?.Session != null)
return ActivatorUtilities.CreateInstance<SessionGridPreferenceStore>(provider);

return ActivatorUtilities.CreateInstance<CookieGridPreferenceStore>(provider);
});
services.AddDataDictionaryServices();
services.AddDataManagerServices();
services.AddEventHandlers();
Expand Down
4 changes: 2 additions & 2 deletions src/MasterData.Core/DataDictionary/Models/GridUI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,12 @@ public class GridUI
public bool EnableMultiSelect { get; set; }

/// <summary>
/// Maintains filters, order and grid pagination in cookies,
/// Maintains grid display settings and navigation state in the configured preference store,
/// and recovers on the first page load. (Default = false)
/// </summary>
/// <remarks>
/// When using this property, we recommend changing the object's [Name] parameter.
/// The [Name] property is used to compose the cookie names.
/// The [Name] property identifies the grid in the preference store.
/// </remarks>
[JsonPropertyName("maintainValuesOnLoad")]
[Display(Name = "Save User Preferences")]
Expand Down
151 changes: 0 additions & 151 deletions src/MasterData.Core/Extensions/CookieExtensions.cs

This file was deleted.

Loading
Loading