Binding extension parameters
Any DotVVM binding is an expression that may use certain identifiers and there are a few types of them:
- Class Names - For example,
System.String
, these don't have any value, you can only access (static) members on them. The set of accessible classes is controlled byNamespaceImport
s, you can set it globally in theDotvvmConfiguration
or locally using@import
directive. - DataContext Parameters - For example,
_this
,_parent4
or_root
. These identifiers have a value and you can do anything with them and the value comes from the list of all data contexts of the invoking control. - Properties/Methods of current view model - For example
MyProperty
. This is just an alias for_this.MyProperty
, the value comes from the data context. - Extension Parameters - For example,
_index
,_api
. This is also an identifier with a value and these parameters can be user-defined. The value is usually computed from some properties on the invoking control and can be used arbitrarily in the binding. The point of this document is to have a look in depth how do they work.
Extension parameter API
The new ExtensionParameter is defined by the BindingExtensionParameter
abstract class. In order to be recognized by the binding compiler, it has to be registered globally in DotvvmConfiguration.Markup.DefaultExtensionParameters
or locally in DataContextStack.ExtensionParameters
. In order to make it useful, we will have to define some methods and values:
identifier
- A name that will be used in binding expression to reference this parametertype
- Type of the parameter. If you'd do the_index
parameter it would beSystem.Int32
, the_page
is of typeBindingPageInfo
. You can, of course, create a new type that will be used specifically for this purpose.inherit
- When the extension parameter is introduced in a specific data context (like the_index
that is only present in Repeater-like controls), this parameter controls if the parameter will also be valid in child data contexts.GetServerEquivalent(Expression controlParameter)
- When the binding is compiled to be used in .NET code, this method is invoked to get an expression that returns the value of the parameter. You can use a reference to the current control in the code.GetJsTranslation(JsExpression dataContext)
- This gets the expression that can be used in the translated Javascript expressions. Here, you can use a reference to the knockout context to compute the value.
For example, you may know the @import
directive - it basically introduces a new extension parameter that represents a service imported from IServiceProvider
. Let's have a look at how we could implement it. First, we will need class inheriting from BindingExtensionParameter
:
public class InjectedServiceExtensionParameter : BindingExtensionParameter
{
...
}
Then, we'll need a constructor that sets the parameters of the base class. We don't need to do anything inside it, and we will let the users decide which type
they want to import and which identifier
they want to give it:
public InjectedServiceExtensionParameter(string identifier, ITypeDescriptor type)
: base(identifier, type, inherit: true) { }
We'll need to implement the runtime behavior of the parameter - which value should the identifier have. The parameter is translated into an expression that may use the current control by the GetServerEquivalent
method:
public override Expression GetServerEquivalent(Expression controlParameter)
{
// Extract System.Type from the ITypeDescriptor, so we can use it to invoke `IServiceProvider.GetService`
var type = ResolvedTypeDescriptor.ToSystemType(this.ParameterType);
// Create expression that invokes the `ResolveStaticCommandService` method that is defined bellow
var expr = ExpressionUtils.Replace((DotvvmBindableObject c) => ResolveStaticCommandService(c, type), controlParameter);
// And cast the result to the expected type
return Expression.Convert(expr, type);
}
// This is a helper method that finds a IServiceProvider in the control tree and resolves a service of the `type`
private object ResolveStaticCommandService(DotvvmBindableObject c, Type type)
{
// The IDotvvmRequestContext is saved in the control tree
var context = (IDotvvmRequestContext)c.GetValue(Internal.RequestContextProperty, true);
return context.Services.GetService(type);
}
The ExpressionUtils.Replace
is a helper method that exploits the fact that we are using standard Linq.Expressions to represent bindings during compilation. It takes the lambda as Expression and simply replaces occurrences of c
with the expression in controlParameter
.
Finally, we'll have to say how should it be translated to Javascript. Unfortunately, there is not a reasonable way to translate this into Javascript, so we will simply throw an exception. It will basically forbid the usage of this extension parameter in bindings translated to Javascript (value
and staticCommand
bindings)
public override JsExpression GetJsTranslation(JsExpression dataContext)
{
throw new InvalidOperationException($"Can't use injected services in javascript-translated bindings.");
}
If you'd like to see implementation of a few more, have a look at the real implementation on Github
Registration
It's very simple to register the extension parameter globally (for all pages), you simply put the modification of DotvvmConfiguration
into a startup class:
config.Markup.DefaultExtensionParameters.Add(
new InjectedServiceExtensionParameter("myCoolService", new ResolvedTypeDescriptor(typeof(MyCoolService))));
The parameters are part of the data context (represented by DataContextStack
class), so they can be added by DataContextChangeAttributes
and you can use to set the extension parameter locally. For example, the _index
and _collection
parameters are created only inside Repeater-like controls. DotVVM compiler tracks the data context for each control and each control may modify the data context that will be inside its children or inside its templates.
This is done by annotating the template property or the control by an attribute that inherits DataContextChangeAttribute
. For example, Repeater
changes data context of its ItemTemplate
into the type of element of the collection bound in DataSource
property. This is done by annotating the property with two change attributes - the changes the type to be the type of the DataSource
property and the second changes the type to be element of that collection:
[ControlPropertyBindingDataContextChange(nameof(DataSource))]
[CollectionElementDataContextChange(1)]
public ITemplate ItemTemplate { ... }
These attributes can also add extension parameters to the content. For example, if we'd like to allow children of our control use some injected service parameter, we could implement the attribute as follows:
public class CollectionElementDataContextChangeAttribute : DataContextChangeAttribute
{
public override int Order { get; }
public CollectionElementDataContextChangeAttribute(int order)
{
Order = order;
}
public override ITypeDescriptor GetChildDataContextType(ITypeDescriptor dataContext, IDataContextStack controlContextStack, IAbstractControl control, IPropertyDescriptor property = null) => null; // we don't want to change the type
public override Type GetChildDataContextType(Type dataContext, DataContextStack controlContextStack, DotvvmBindableObject control, DotvvmProperty property = null) => null;
public override IEnumerable<BindingExtensionParameter> GetExtensionParameters(ITypeDescriptor dataContext) =>
return new BindingExtensionParameter[] {
new InjectedServiceExtensionParameter("myService", new ResolvedTypeDescriptor(typeof(IMyService)))
};
}