Pickstack is a set of replaceable ASP.NET Core components. An app picks a file manager and a rich text editor, and can later swap either one by changing a package reference and a registration. File fields and editors keep working because they talk to Pickstack, not to elFinder, Responsive File Manager, WebFilez, TinyMCE, or CKEditor.
A page renders two tags:
<pickstack-file asp-for="CoverImage" file-type="Image" />
<pickstack-editor asp-for="Body" />Those tag helpers ask dependency injection for IFileManager and IRichTextEditor. The registered adapter emits its own styles, scripts, and a small provider script.
Every file manager provider implements the same browser function:
Pickstack.files.open({
type: "image",
callback: function (file) { /* file.url */ },
onClose: function () { /* dismissed without a choice */ }
});callback runs when a file is chosen. onClose runs when the dialog is dismissed without one: the close button, Escape, a click outside it, or another open replacing it. A choice does not also call onClose.
The cover-image button calls that. TinyMCE's file picker calls that. CKEditor opens a Pickstack browse page that mounts the same manager. Replacing the manager replaces the function, so every picker on the site follows.
Both adapters read and write wwwroot/media (URL /media/). Files uploaded with one manager are still there after the swap.
Pickstack.Abstractions shared client assets and component contract
Pickstack.FileManager contract, storage, tag helpers
Pickstack.FileManager.elFinder elFinder.NetCore
Pickstack.FileManager.ResponsiveFileManager Responsive File Manager
Pickstack.FileManager.WebFilez WebFilez, referenced as projects until the package is published
Pickstack.RichTextEditor contract and tag helper
Pickstack.RichTextEditor.TinyMce shared TinyMCE adapter
Pickstack.RichTextEditor.TinyMce5 TinyMCE 5.10.9 and a local copy
Pickstack.RichTextEditor.TinyMce6 TinyMCE 6.8.5 and a local copy
Pickstack.RichTextEditor.TinyMce7 TinyMCE 7.9.0 and a local copy
Pickstack.RichTextEditor.TinyMce8 TinyMCE 8.10.1 and a local copy
Pickstack.RichTextEditor.Froala shared Froala adapter
Pickstack.RichTextEditor.Froala5 Froala 5.5.0 and a local copy
Pickstack.RichTextEditor.CkEditor browse page that hosts IFileManager
Pickstack composition, shared browser runtime
An editor package does not reference an elFinder, Responsive File Manager, or WebFilez package. It only uses Pickstack.files.
The demo references every adapter so you can switch without changing project references. In Pickstack.Demo/appsettings.json:
"Pickstack": {
"FileManager": "elFinder",
"RichTextEditor": "TinyMCE8"
}FileManager is elFinder, ResponsiveFileManager, or WebFilez. RichTextEditor is TinyMCE5, TinyMCE6, TinyMCE7, TinyMCE8, Froala5, or CKEditor. TinyMCE selects TinyMCE 8, and Froala selects Froala 5. Restart the app after changing them.
Each version package loads that editor from the CDN, then falls back to the copy shipped in the package. TinyMCE serves /_content/Pickstack.RichTextEditor.TinyMce8/tinymce.min.js (and the same path for versions 5, 6, and 7), including skins and plugins. Froala 5 serves /_content/Pickstack.RichTextEditor.Froala5/froala_editor.pkgd.min.js and its stylesheet. The editor still starts when the CDN is unreachable.
Pickstack:Storage is the shared upload folder. WebRootFolder can be nested, such as Media/Uploads. Keep the thumbnail folder outside the upload folder; Responsive File Manager requires that.
Responsive File Manager's NuGet package targets .NET 9. The adapter project sets AssetTargetFallback so this .NET 10 solution can reference it.
Reference Pickstack, the file-manager contract, the editor contract, and one adapter for each. Then register them:
builder.Services.AddControllersWithViews();
builder.Services.AddPickstack(builder.Configuration)
.AddElFinder()
.AddTinyMce8();
var app = builder.Build();
app.UseStaticFiles();
app.UsePickstack();
app.UseRouting();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");UseStaticFiles() is what serves files saved under wwwroot/media. UsePickstack() creates those folders and serves the shared browser script. The Responsive File Manager adapter also starts its session and PHP middleware there, so the host should not call UseSession itself.
Add the tag helpers in _ViewImports.cshtml:
@addTagHelper *, Pickstack.FileManager
@addTagHelper *, Pickstack.RichTextEditorelFinder's browser needs jQuery on the page before the picker scripts. The demo loads it in the layout head. The CKEditor browse window loads its own copy.
WebFilez runs in a frame, so the host page does not load Blazor. AddWebFilez() registers the Blazor Server circuit, and UsePickstack() maps that circuit and the WebFilez API. The web app must set <RequiresAspNetWebAssets>true</RequiresAspNetWebAssets>. .NET 10 otherwise omits blazor.server.js. The adapter references the WebFilez projects. When the packages are published, replace those project references with package references.
To swap elFinder for another manager, change the package reference and .AddElFinder() to .AddResponsiveFileManager() or .AddWebFilez(). Leave the views alone.
Each Add method accepts a fluent options object for that product. These settings apply to every instance. A field can override them:
<pickstack-file asp-for="CoverImage" file-type="Image" file-root="42/7" />
<pickstack-files file-type="Folder" file-root="42/7" />
<pickstack-editor asp-for="Body" file-root="42/7" add-plugins="contentzone" toolbar="undo redo | contentzone" />file-root is a relative folder under the upload directory. 42/7 is a tenant and user; a single segment works the same way. file-type="Folder" shows directories. Clicking a folder opens it, and Use this folder writes the open folder's path into the field. A <pickstack-editor> height, plugins, add-plugins, or toolbar attribute applies to that field only. The file picker stays connected to Pickstack.
Same-origin file URLs are stored as root-relative paths, such as /media/42/7/photo.png. TinyMCE keeps that form by removing the script host.
builder.Services.AddPickstack(builder.Configuration)
.AddElFinder(files => files
.DisableCommands("extract", "archive")
.AddToolbarGroup("fullscreen"))
.AddTinyMce8(editor => editor
.AddPlugins("codesample", "charmap")
.WithToolbar("undo redo | bold italic | codesample charmap | link image")
.Set("branding", false));Set passes a value through to the underlying init object. Named settings such as WithToolbar are applied after it.
CKEditor groups buttons the same way. An empty toolbar leaves CKEditor's default toolbar in place.
.AddCkEditor(editor => editor
.WithoutPlugins("elementspath")
.WithToolbar(
["Bold", "Italic", "Underline"],
["Link", "Image"]));Froala uses the same field attributes. Image, video, and file buttons open the file manager. WithLicenseKey supplies a Froala license key.
.AddFroala5(editor => editor
.AddPlugins("codeSnippet")
.AddToolbarButtons("codeSnippet"));Responsive File Manager's dialog options are fluent too. Server configures the PHP package, including upload limits:
.AddResponsiveFileManager(files => files
.WithLanguage("en_EN")
.Server(server => server.MaxSizeUpload = 16));Call .UseHostSession() when the host already calls UseSession(), as Mantle does. Pickstack then leaves that session in place.
WebFilez uses the same upload folder. file-root is the top of that browser: the user cannot navigate above it. Image fields ask for image/*. Folder fields show directories, and Use this folder writes the open folder. .Server configures WebFilez itself, including the upload limit. .UseHostBlazor() and .UseHostApi() leave an existing Blazor circuit or MapWebFilezApi() call in place.
.AddWebFilez(files => files
.WithTheme("auto")
.Server(server => server.MaxUploadBytes = 32 * 1024 * 1024));That root is always the shared Pickstack folder. AddPickstack binds Pickstack:Storage to IFileStorage, and the WebFilez adapter publishes it as the media root on the filesystem provider. file-root (such as a tenant folder) is created on that disk before the browser opens. Swapping to elFinder or Responsive File Manager still sees those files.
"Pickstack": {
"Storage": {
"RequestPath": "/Media/Uploads/",
"WebRootFolder": "Media/Uploads",
"ThumbnailRequestPath": "/Media/Thumbs/",
"ThumbnailWebRootFolder": "Media/Thumbs"
}
}WebRootFolder is a directory under the web root. Keep the thumbnail folder outside it.
WebFilez can also show a root stored somewhere else. Register the provider package, then add the root inside Server. FileRoot is WebFilez.Services.FileRoot. The adapter does not read a WebFilez configuration section, so this callback is where the root is declared. Azure Blob:
builder.Services.AddWebFilezAzureBlob();
builder.Services.AddPickstack(builder.Configuration).AddWebFilez(files => files.Server(server =>
{
server.MaxUploadBytes = 32 * 1024 * 1024;
server.Roots.Add(new FileRoot
{
Id = "assets",
Name = "Assets",
Provider = "azure-blob",
Path = "uploads",
PublicBaseUrl = "https://cdn.example.com",
Settings = new Dictionary<string, string>
{
["ConnectionString"] = builder.Configuration["Azure:Storage"]!,
["Container"] = "media"
}
});
}));assets appears beside media. The same shape works for the other provider packages:
| Package | Registration | Provider |
Settings |
|---|---|---|---|
WebFilez.Storage.AzureBlob |
AddWebFilezAzureBlob() |
azure-blob |
ConnectionString, Container |
WebFilez.Storage.AmazonS3 |
AddWebFilezAmazonS3() |
amazon-s3 |
AccessKey, SecretKey, Bucket |
WebFilez.Storage.GoogleCloud |
AddWebFilezGoogleCloud() |
google-cloud |
Bucket |
WebFilez.Storage.AzureFiles |
AddWebFilezAzureFiles() |
azure-file |
ConnectionString, Share |
Path is a prefix inside that store. PublicBaseUrl is the address written into content, such as a CDN host.
This does not replace media. The picker, file-root, and the standalone browser still open the local folder. Registering a different IFileStorage before AddPickstack only changes which directory that is. The contract is a physical path, so it cannot point the shared root at a blob container or a bucket.
A file manager implements IFileManager:
GetClientAssets()returns the styles and scripts it needs.ProviderScriptUrlpoints at a script, usually embedded and served from/pickstack/assets/{name}.GetProviderConfiguration()returns JSON for that script. UseJsonSerializerso<is escaped.- The script calls
Pickstack.files.use({ mount: function (element, options) { ... } }). options.selectmeans the user is choosing a file. CallPickstack.files.notify({ url, name })when they do.options.typeisany,file,image,media, orfolder.options.rootis the relative folder to open, such as42/7. Pickstack creates it before callingmount.- Register the class as
IFileManagerand asIPickstackAssetSourceif the script is embedded.AddFileManager<T>()only registers the interface; an adapter with endpoints or middleware should add its own extension, asAddElFinderdoes.
An editor implements IRichTextEditor and registers with Pickstack.editors.use({ init: function (element, options) { ... } }). When it needs a file, it calls Pickstack.files.open instead of embedding a file-manager URL.
If you find this project useful, consider supporting its development.
Prefer using your phone? Scan the QR code:
