FluentDecorator 1.0.1

dotnet add package FluentDecorator --version 1.0.1
NuGet\Install-Package FluentDecorator -Version 1.0.1
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FluentDecorator" Version="1.0.1" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add FluentDecorator --version 1.0.1
#r "nuget: FluentDecorator, 1.0.1"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install FluentDecorator as a Cake Addin
#addin nuget:?package=FluentDecorator&version=1.0.1

// Install FluentDecorator as a Cake Tool
#tool nuget:?package=FluentDecorator&version=1.0.1

FluentDecorator (created when doing Blazor). Why?

I was creating a Blazor project with some rendering logic. I was setting readonly, disabled, class etc. attributes to variables and methods. It was spread across the markup file. I was wondering whether this kind of rendering logic couldn't be defined more centralized and made descriptive, given you more inside what the rendering logic actually is about and what it involves.

Secondly with Blazor the distinction between server-side and client-side objects disappears. You can use one class for both. But both sides keep their own context. Readonly server-side is used for properties ressembling database keys, while at the client side it targets rendering purposes.

The solution

The solution to both was: use a shared model for client and server-side, but decorate the model on the client side with rendering properties. Secondly, define these decorators by using a fluent interface. In the end it looks like (c# class behind code):

            modelDecorationDefinition = ModelDecorators.Init<Ship, ExtendedDecorators>()
                .OnModelIfThenElse(WhenThereIsStock(), EnableDisembark(), DisableDisembark())
                .OnModelPropIfThenElse(s => s.NextInRowToApproveDisembark() is Captain,
                    s => s.Captain,
                    prp => prp.Class = classShow,
                    prp => prp.Class = classHide)
                .OnModelPropIfThenElse(s => s.Status == DisembarkStatus.WaitOnSteerman,
                    s => s.Steersman,
                    prp => prp.Class = classShow,
                    prp => prp.Class = classHide)
                .OnModelPropWhen(s =>
                    s.Status >= DisembarkStatus.DisembarkCanStart
                    && s.Status < DisembarkStatus.DisembarkFinished
                    , s => s.Status
                    , dec => dec.Visible = true);

            modelDecorated = modelDecorationDefinition(ship);

and the markup code could be:

    <div class="@ModelDecorated.GetFor(s => s.Captain).Class">
        @*etc. etc.*@
    </div>
    @if (modelDecorated.GetFor(x => x.Status).Visible)
    {
        @*etc. etc.*@
    }

Solution explained

The model (in this case ship) isn't changed, it doesn't have rendering properties like readonly and enabled. Model logic still resides in the model class, for example: NextInRowToApproveDisembark and EvaluateStatus.

The rendering logic is centralized in modelDecorationDefinition. The model is decorated (extended if you want) with properties from the ExtendedDecorators class (Enabled, Visible and Class). This is defined by the generic type parameters of the ModelDecorators.Init<Ship, ExtendedDecorators> method. Instead of the ExtendedDecorators, you can use any other class as long as it inherits from the DecoratorsAbstract class, which is a marker class (= no properties in it). In other words you are completly free in defining your own decoration properties.

The decorations are done on the class itself and the properties of the class. If a property is a reference type, like Captain, the decoration descends also these classes ending up with an ExtendedDecorators instance for the model, every property of the model and every property of the property chain like Captain etc.. IEnumerables are excluded from this chaining mechanism.

Definition versus Execution

The fluent interface which starts with ModelDecorators.Init<TI, TS>, is a func delegate. It is nothing else but piped function pointers, which are only executed when invoked with a TI instance (in our case a ship).

You can extend the fluent interface with your own functions. Say you want to combine two decorator you just define

        public static WithDecorators<TI, IModelDecorators<TI, TS>> Combine<TI, TJ, TS>(
            this WithDecorators<TI, IModelDecorators<TI, TS>> source,
            WithDecorators<TJ, IModelDecorators<TJ, TS>> combineWith,
            TJ combinedValue)
            where TI : new()
            where TJ : new()
            where TS : DecoratorsAbstract, new()
        {
            return input =>
            {
                var ret = source(input);
                var retCombineWith = combineWith(combinedValue);
                //do your combining stuff here
                return ret;
            };
        }


When do we want to execute this pipeline? In Blazor, you probably want it after a model change, but just before the rendering starts. The ShouldRender override seems the perfect place. If the state changes and is rendered, your model decorations are updated as well.

        protected override bool ShouldRender()
        {
            modelDecorated = modelDecorationDefinition(ship);
            return base.ShouldRender();
        }

Example

I included an example which also deals with IEnumerables (in which you add decorations item based).

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 is compatible. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETCoreApp 3.1

    • No dependencies.
  • .NETStandard 2.0

    • No dependencies.
  • .NETStandard 2.1

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.1 480 2/4/2020
1.0.0 393 2/3/2020

Changes in 1.01
* Added attributes for limiting the properties to including in decoration

First implementation with only core functionality