I made a quick change to my CG.Blazor.Forms library. It’s a change I need for a project I’m building that makes good use of the dynamic form generation capabilities of my library.
The source code for the CG.Blazor.Forms library can be found HERE.
The NUGET package for the CG.Blazor.Forms library can be found HERE.
An introduction to the CG.Blazor.Forms library can be found HERE.
So, what did I change? I added a new property to the RenderObjectAttribute
class, called VisibleExp
. That new property allows a developer to show or hide the object associated with the attribute, at render time, based on the value of another property, on the same view-model.
Let’s look at what I’m saying. We’ll start with an example view-model:
public class ExampleVM { [RenderMudSelect(Options = "Item A, Item B")] public string Selected { get; set; } [RenderObject(VisibleExp = "x => x.Selected=\"Item A\"")] public ModelA A { get; set; } [RenderObject(VisibleExp = "x => x.Selected=\"Item B\"")] public ModelB B { get; set; } public ExampleVM() { Selected = nameof(A); A = new SqlServerModel(); B = new MongoModel(); } } public class ModelA { [RenderMudTextField] public string A {get; set; } } public class ModelB { [RenderMudTextField] public string B {get; set; } }
ExampleVM
is a typical view-model for a dynamic form. The properties on the class are decorated with RenderObjectAttribute
attributes that provide the form generator with hints about how to render the form. The ModelA
and ModelB
classes are just a quick way to get some content that we can flip between, at runtime.
Next, we’ll generate a dynamic Blazor UI using some markup, like this:
<DynamicComponent Model="@Model" /> @code { ExampleVM Model { get; set; } }
That markup creates a simple UI, using the DynamicComponent
component, that looks something like this:
The “Selected” field is a dropdown that contains two options: “Item A”, and “Item B”. When the second option is chosen, the lower part of the UI automatically renders itself like this:
That, in itself, may not seem like much, but, what’s happening is, every time Blazor renders our dynamic form, the RenderObjectAttribute
attribute, on the ExampleVM
view-model, is checking the VisibleExp
property for a LINQ expression that it can use to determine whether to render the associated object (or not). When it finds a valid LINQ expression, it invokes it to determine whether to render the associated object, or not.
Recall, that the VisibleExp
property for the first object (the A
property on ExampleVM
), looks like this:
VisibleExp = "x => x.Selected=\"Item A\""
That means, whenever the Selected
property, on ExampleVM
contains the string “Item A”, the A
property will be rendered.
The VisibleExp
property for the second object (the B
property, on ExampleVM
) looks like this:
VisibleExp = "x => x.Selected=\"Item B\""
That means, whenever the Selected
property, on ExampleVM
contains the string “Item B”, the B
property will be rendered.
So how did I do that? Let’s look at that now:
In our example, above, the RenderObjectAttribute
class was used to decorate object properties that should be rendered. That’s needed because, by default, the form generator, that drives the operation of the DynamicComponent
component, simply ignores all undecorated properties on the view-model. It’s a bit more complicated than that, actually. It turns out, it’s not enough to be told to render a property, the form generator also needs to know how to render the property. That’s really the biggest reason properties on the view-model have to be decorated, in order to be seen by the form generator and rendered on the resulting Blazor form.
So how does RenderObjectAttribute
work? Let’s start by looking at the source:
[AttributeUsage( AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false)] public class RenderObjectAttribute : FormGeneratorAttribute { public string VisibleExp { get; set; } public override int Generate( RenderTreeBuilder builder, int index, IHandleEvent eventTarget, Stack<object> path, PropertyInfo prop, ILogger<IFormGenerator> logger ) { // Validate the parameters before attempting to use them. Guard.Instance().ThrowIfNull(builder, nameof(builder)) .ThrowIfLessThanZero(index, nameof(index)) .ThrowIfNull(path, nameof(path)) .ThrowIfNull(logger, nameof(logger)); try { // If we get here then we are trying to render an entire object, // one child property at a time. // Should never happen, but, pffft, check it anyway. if (false == path.Any()) { // Let the world know what we're doing. logger.LogDebug( "RenderObjectAttribute::Generate called with an empty path!" ); // Return the index. return index; } // Get the model reference. var model = path.Peek(); // Get the model's type. var modelType = model.GetType(); // Is the IsVisible expression itself invalid? if (false == TryVisibleExp( path, out var isVisible )) { // Let the world know what we're doing. logger.LogDebug( "Not rendering a '{PropType}' object. [idx: '{Index}'] object " + "since the 'VisibleExp' property contains a malformed LINQ expression. " + "The expression should be like: x => x.Property = \"value\"", modelType.Name, index ); // Return the index. return index; } // Is the IsVisible expression false? if (false == isVisible) { // Let the world know what we're doing. logger.LogDebug( "Not rendering a '{PropType}' object. [idx: '{Index}'] object " + "since the 'VisibleExp' property contains a LINQ expression that " + "resolves to false. ", modelType.Name, index ); // Return the index. return index; } // Let the world know what we're doing. logger.LogDebug( "Rendering child properties for a '{PropType}' object. [idx: '{Index}']", modelType.Name, index ); // Get the child properties. var childProps = modelType.GetProperties() .Where(x => x.CanWrite && x.CanRead); // Loop through the child properties. foreach (var childProp in childProps) { // Create a complete property path, for logging. var propPath = $"{string.Join('.', path.Reverse().Select(x => x.GetType().Name))}.{childProp.Name}"; // Get the value of the child property. var childValue = childProp.GetValue(model); // Is the value missing? if (null == childValue) { // If we get here then we've encountered a NULL reference // in the specified property. That may not be an issue, // if the property is a string, or a nullable type, because // we can continue to render. // On the other hand, if the property isn't a string or // nullable type then we really do need to ignore the property. // Is the property type a string? if (typeof(string) == childProp.PropertyType) { // Assign a default value. childValue = string.Empty; } else if (typeof(Nullable<>) == childProp.PropertyType) { // Nothing to do here, really. } // Otherwise, is this a NULL object ref? else if (childProp.PropertyType.IsClass) { // Let the world know what we're doing. logger.LogDebug( "Not rendering property: '{PropPath}' [idx: '{Index}'] " + "since it's value is null!", propPath, index ); // Ignore this property. continue; } } // Push the property onto path. path.Push(childValue); // Look for any form generation attributes on the view-model. var attrs = childProp.GetCustomAttributes<FormGeneratorAttribute>(); // Loop through the attributes. foreach (var attr in attrs) { // Render the property. index = attr.Generate( builder, index, eventTarget, path, childProp, logger ); } // Did we ignore this property? if (false == attrs.Any()) { // Let the world know what we're doing. logger.LogDebug( "Not rendering property: '{PropPath}' [idx: '{Index}'] " + "since it's not decorated with a FormGenerator attribute!", propPath, index ); } // Pop property off the path. path.Pop(); } // Return the index. return index; } catch (Exception ex) { // Provide better context for the error. throw new FormGenerationException( message: "Failed to render an object! " + "See inner exception(s) for more detail.", innerException: ex ); } } private bool TryVisibleExp( Stack<object> path, out bool result ) { // Make the compiler happy. result = true; // Is the expression missing, or empty? if (string.IsNullOrEmpty(VisibleExp)) { // No expression is ok. return true; } // If we get here then we've determined there is a LINQ // expression in the VisibleExp property, so we need to // parse it now, and invoke the resulting Func to get // the results. // Get the view-model reference. var viewModel = path.Skip(1).First(); // Get the view-model's type. var viewModelType = viewModel.GetType(); // Look for the expression parts. var parts = VisibleExp.Split("=>") .Select(x => x.Trim()) .ToArray(); // There should be 2 parts to a LINQ expression. if (2 == parts.Length) { // Create the parameter expression. var x = Expression.Parameter( viewModelType, parts[0] ); // Parse the labmda expression. var exp = DynamicExpressionParser.ParseLambda( new[] { x }, null, parts[1] ); // Compile and invoke the expression. result = (bool)exp.Compile().DynamicInvoke( viewModel ); // We have a valid result. return true; } // The expression was invalid. return false; } }
Let’s start by walking through the Generate
method. Generate
is common to all form generation attributes and is inherited as part of the FormGeneratorAttribute
base class. The form generator itself calls the Generate
method as part of the form generation process. We’ve overridden Generate
, in RenderObjectAttribute
, to step into the associated object property, and then, walk through all the properties on that type, rendering as we go.
We start by validating any incoming parameters. Then we do some deeper sanity checking of the path
variable. That variable contains a path to the thing we’re actively trying to render.
After assuring ourselves that our incoming parameters are all valid, we then grab a reference to the parent object (called model
). After that, we get the type for the model
. We’ll use those two values, model
and modelType
, to walk through the properties for the object we’re trying to render.
This is where our new property, VisibleExp
comes into play …
The first thing we need to determine is whether we should render the current object, or not. If VisibleExp
is empty, then the answer is yes, we should. On the other hand, if VisibleExp
contains a LINQ expression, and if that expression evaluates to FALSE, then the answer is no, we shouldn’t. We make that determination by calling a method called TryVisibleExp
. We’ll cover TryVisibleExp
shortly, for now though, just know that it makes the determination, for us, about whether we should continue to render, or stop.
Assuming we should stop, we do that, logging the decision, and returning the current index value.
Assuming we should continue, we log that we’re about to do some rendering, then we grab all the properties on the modelType
that have a getter
and a setter
(readable and writeable). Once we’ve done that, we iterate through that collection of properties.
For each property, we create a path (of sorts) to the property. We’ll need that for logging purposes, later. Next we get the value of the property. If the property is null we need to do some checking to ensure that we can still render something. It’s difficult to render NULL, so sometimes we have to stop at that point. If we can continue rendering the property, or if the value isn’t NULL. we push the current value into the path
variable, then we go look for any FormGeneratorAttributes
on the property.
For each attribute we find, we then call Generate
on that attribute. In this way, we use a bit of recursion to make everything that much easier to code, and that much more efficient. Of course, efficient doesn’t necessarily equate to “easier to understand”, but, I suppose there are always tradeoffs, right?
After we’ve finished rendering the property, we pop the current property value off our path
variable and move on to the next property.
But wait, where is the code that actually renders the property?? Well, if the property we’re rendering is another object, and that property is also decorated with a RenderObjectAttribute
attribute, then we’ll recursively step into that child property and keep going – until we either stack fault or run out of descendant properties to render. My guess is, developers will get tired of adding child properties, and grandchild properties, and great-grandchild properties, and so on, long before the stack itself faults. At least I hope that’s the case. :o)
Eventually, one of those descendant properties will contain a property that returns a primitive type. When that happens, assuming the property was decorated with a kind of FormGeneratorAttribute
attribute, then that attribute will render the property as a UI component, of one sort or another. For instance, our dropdown list is rendered with a RenderMudSelectAttribute
attribute, and our a text box fields are both rendered with RenderMudTextFieldAttribute
attributes.
And that’s how the rendering actually takes place.
The only thing I haven’t talked about, so far, is the implementation of the TryVisibleExp
method. Looking at that code now, we see that the method is laid out like any other .NET TryXXX method, where it returns TRUE if the outgoing property is valid, and FALSE if it isn’t. In our case, we return TRUE if the VisibleExp
property contains a LINQ expression, and that expression is valid, and FALSE if those two things are not true.
In order to evaluate the LINQ expression, we need a reference to the parent of this model. We get that from the path
variable. After that, we get the type of the view-model. We’ll need those type bits of information when we create the LINQ expression.
Next, we split the value of the VisibleExp
property up using the => symbol. That gives us the name of the property on the left hand side, and the expression itself on the right hand side.
We then use the variable (left hand side of VisibleExp
) to create an actual LINQ parameter expression.
After that, we use the call to DynamicExpressionParser.ParseLambda
to turn the right hand side of the VisibleExp
property into an actual LINQ expression. Having written this sort of code, by hand, in the past, I am very grateful to Microsoft for the DynamicExpressionParser
class. It’s much easier than writing expression code, trust me.
After we have the LINQ lambda, expression, we just have to compile it, then call it, to get the results of the operation, which we store in the outgoing result
variable.
The result of this new parameter, VisibleExp
, is that we can now create dynamic forms that can decide, at render time, which properties to render, and which ones not to. That because important as the complexity of dynamic forms increases, which they seem to do whether we like it or not.
The alternative to this approach is quite complicated since dynamic forms are, by their nature, somewhat self contained, and therefore, not easy to extend with Blazor code or java script.
Photo by Joran Quinten on Unsplash