Code-only controls
This kind of controls is useful when you need to render more complex or dynamic HTML, or when you need to support complex data-binding scenarios, or manipulate the viewmodel on the client-side.
Building code-only controls is more difficult, but they can do much more. All built-in DotVVM controls are implemented as code-only controls.
If you want to learn about how to write controls in DotVVM, we encourage you to look in the DotVVM GitHub repository, on in the DotVVM Contrib repo to see how the built-in controls are implemented.
Fundamentals
In general, all controls in DotVVM
inherit from the DotvvmControl
class. This base class provides only basic functionality and it is not a good base class to inherit directly for most purposes.
The most useful class to be derived from is HtmlGenericControl
(which inherits from DotvvmControl
). It is prepared to render one HTML element, and can contain child elements or controls. Most built-in controls in DotVVM derive from the HtmlGenericControl
class.
Register controls
First, you need to register the code-only control in the DotvvmStartup.cs
file.
config.Markup.AddCodeControls("cc", typeof(DotvvmDemo.Controls.Control));
Using this code snippet, if you use the <cc:
tag prefix, DotVVM will search for the control in the specified namespace and assembly.
If you register a markup control with code behind like this, it won't work correctly. If the control has a markup, it must be registered using the
AddMarkupControl
method. See the Markup control registration chapter for more details.
Create code-only control
The best example to learn how to write controls in DotVVM is to look how the built-in controls are implemented. Some of them are pretty complicated, but there are some which are simple.
For the purpose of this chapter, let's begin with implementing a control similar to the TextBox control. The real TextBox
control included in the framework is much more complex as it supports formatting values or different kinds of input
element, but let's focus only on the main functionality - providing the way to edit a string value in the viewmodel.
In order to use the control in the browser, we need it to produce the following markup:
<input type="text" data-bind="value: FirstName" />
This is what we'd like to render when we see <cc:SimpleTextBox Text="{value: FirstName"}" />
.
So let's create a class that derives from HtmlGenericControl
. In the constructor, we call the base constructor and tell the name of the HTML element we want the control to render: input
.
public class SimpleTextBox : HtmlGenericControl
{
public SimpleTextBox() : base("input")
{
}
}
This would render just <input></input>
in the page. Also, if you add any custom attributes (e.g. style
, class
) on the TextBox
control, it would append them in the page. The HtmlGenericControl
takes care about all this stuff for you.
HTML rendering pipeline
Now, the HtmlGenericControl
has 4 methods which we can override to modify the rendered HTML. They are called in this order:
AddAttributesToRender
- by default, this method takes all HTML attributes set to the control, and prepares them to be rendered.RenderBeginTag
- by default, this method renders the begin tag.RenderContents
- by default, this method renders the child controls.RenderEndTag
- by default, this method renders the end tag.
HtmlWriter
Now, let's see how to render the HTML element. In DotVVM, we use the HtmlWriter
object to generate HTML. To render the <input type="text" />
we need to call something like this:
writer.AddAttribute("type", "text");
writer.RenderSelfClosingTag("input");
Notice that the flow is this: first add the attributes we want to render, and then render the tag. The HtmlWriter
stores the attributes in a temporary buffer, and emits them when you decide what tag you want to render.
There are also methods RenderBeginTag("input")
, RenderEndTag()
, WriteText("some text")
or WriteUnencodedText("some HTML")
.
The AddAttribute
method is called before rendering the tag and it also has a third argument called append
. If you call AddAttribute("class", "blue")
and then AddAttribute("class", "red", append: true)
, the classes will be appended. The HtmlWriter
knows that values in the class
HTML attribute are separated by spaces, values in the style
attribute by semicolons etc. You can also specify your own separator character as the fourth argument.
Render HTML
Let's continue with our SimpleTextBox
class. We don't want to render begin and end tags <input></input>
, but just the self closing one <input />
.
Also, it doesn't make sense to allow the TextBox
to have any content inside. We can decorate the class with the [ControlMarkupOptions(AllowContent = false)]
attribute to tell DotVVM that there should be no content inside the <dot:TextBox>
and </dot:TextBox>
tags. If the user places anything there, DotVVM will display an error page.
We can override the RenderBeginTag
method to render the self-closing tag, and the RenderEndTag
to render nothing.
Between these two methods, the rendering pipeline calls also the RenderContents
method which renders the contents between the <dot:TextBox>
and </dot:TextBox>
tags, but we won't have anything here thanks to the ControlMarkupOptions
attribute.
[ControlMarkupOptions(AllowContent = false)]
public class SimpleTextBox : HtmlGenericControl
{
public SimpleTextBox() : base("input")
{
}
protected override void RenderBeginTag(IHtmlWriter writer, IDotvvmRequestContext context)
{
// TagName contains the value passed to the base constructor.
// We don't want to call base.RenderBeginTag here because it would render the begin tag and then the closing tag.
// We want the self closing tag.
writer.RenderSelfClosingTag(TagName);
}
protected override void RenderEndTag(IHtmlWriter writer, IDotvvmRequestContext context)
{
// do nothing, we have already rendered the self-closing tag
}
}
HTML attributes
The most interesting is the AddAttributesToRender
method.
The default implementation takes all HTML attributes that are not mapped to DotVVM properties, and add them to the HtmlWriter
. So, if the user uses the following snippet, the default implementation of AddAttributesToRender
will add the class
, style
and placeholder
attributes
to the HtmlWriter
.
Remember that
HtmlWriter
requires to add all attributes before we callRenderBeginTag
. After you render a tag, you cannot go back and add any attributes to it.
The custom attributes even support data-bindings, so you don't have to care about this. You just need to take care of the control properties.
<cc:SimpleTextBox Text="{value: FirstName}" style="border: none" class="txb1" placeholder="Enter first name" />
We need to declare the Text
property first:
public string Text
{
get { return Convert.ToString(GetValue(TextProperty)); }
set { SetValue(TextProperty, value); }
}
public static readonly DotvvmProperty TextProperty =
DotvvmProperty.Register<string, SimpleTextBox>(t => t.Text, "");
However, we should support two scenarios:
<cc:SimpleTextBox Text="{value: FirstName}" />
<cc:SimpleTextBox Text="Test" />
In the first case, we need to render data-bind="value: FirstName"
, in the second case we need to render value="Test"
.
We can solve this like this:
protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context)
{
var textBinding = GetValueBinding(TextProperty);
if (textBinding != null)
{
// the property contains binding - this will render data-bind="value: expression"
writer.AddKnockoutDataBind("value", this, TextProperty);
}
else
{
// render the value in the HTML
writer.AddAttribute("value", Text);
}
writer.AddAttribute("type", "text");
base.AddAttributesToRender(writer, context);
}
Because this pattern is quite usual, and in most controls you would have written the if
statement checking the presence of binding and rendering the appropriate output, there is an overload of the AddKnockoutDataBind
method with four arguments.
It allows you to specify a function which is called when the specified property doesn't contain a binding.
So we could simplify the function above like this:
protected override void AddAttributesToRender(IHtmlWriter writer, IDotvvmRequestContext context)
{
writer.AddKnockoutDataBind("value", this, TextProperty, () => {
writer.AddAttribute("value", Text);
});
writer.AddAttribute("type", "text");
base.AddAttributesToRender(writer, context);
}
Thanks to this, the syntax is much shorter.
Build the inner control tree
Rendering HTML using the HtmlWriter
class is good for simple controls. If the control is more complicated or can contain controls which invoke postbacks, you need to build a control tree inside the control.
This is especially handy if you need to compose a complex control of already existing ones.
This approach often results in a cleaner code, but rendering the HTML using the HtmlWriter
is much faster than creating a control for the <div>
element using new HtmlGenericControl("div")
.
You need to decide if rendering raw HTML is OK for your case, or if the control is more complex and you need to build a tree of child controls and manipulate them somehow.
Let's create a control that is composed of two existing controls (TextBox
and Literal
) placed inside a div
element.
We will create a new class which derives from HtmlGenericControl
and renders a div
:
public class TextBoxWithLabel : HtmlGenericControl
{
public TextBoxWithLabel() : base("div")
{
}
}
Next, let's add the Text
and LabelText
properties.
Both of them are required. We can indicate this by using the MarkupOptions
attribute. The attribute can also specify whether the property can contain a data-binding or a hard-coded value or both. By default, it can contain both of them.
[MarkupOptions(AllowHardCodedValue = false)]
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DotvvmProperty TextProperty
= DotvvmProperty.Register<string, TextBoxWithLabel>(c => c.Text, null);
[MarkupOptions(AllowBinding = false)]
public string LabelText
{
get { return (string)GetValue(LabelTextProperty); }
set { SetValue(LabelTextProperty, value); }
}
public static readonly DotvvmProperty LabelTextProperty
= DotvvmProperty.Register<string, TextBoxWithLabel>(c => c.LabelText, null);
Child controls
Similarly to the viewmodels, every control has lifecycle events OnInit
, OnLoad
and OnPreRender
which follow the logic of the viewmodel Init
, Load
and PreRender
events.
A basic rule is to create the controls as soon as possible. If you don't need data from the viewmodel (which are deserialized after the Init
event), build the child controls in the OnInit
phase. If you rely on values entered by the user, build the controls in the OnLoad
phase.
protected override void OnInit(IDotvvmRequestContext context)
{
var textBox = new TextBox();
// copy the binding from the control's Text property to the TextBox.Text property
textBox.SetBinding(TextBox.TextProperty, GetValueBinding(TextProperty));
// we can set LabelText now, it cannot contain binding
var label = new Literal(LabelText);
// the controls are always the same, they don't depend on the viewmodel data, so we can use the OnInit event
Children.Add(label);
Children.Add(textBox);
base.OnInit(context);
}
After the Load
phase, the commands are executed and the control tree must be complete at that moment. Moreover, the control tree must be equal as it was in the previous postback, otherwise DotVVM won't be able to find the control which triggered the postback. DotVVM validates postback information and if the control doesn't exist in the page, an error page shows up and the postback is not processed.