ViewBuilder
ViewBuilder creates Wisej.NET views at runtime from a declarative definition, either JSON or a dynamic object model, instead of designer-generated code. One definition can describe:
- the whole control tree, from the form down to its nested controls;
- non-visual components, such as a
ToolTip, aBindingSourceor anErrorProvider, and their extender properties; - event handlers, by method name;
- data bindings, with a
{Binding …}expression.
Use it where the shape of a screen comes from data rather than from the designer: form layouts stored in a database, screens configured per customer, forms generated by a back-end service or an AI model, or plugin UIs loaded at runtime.
Installation
Add the Wisej-4-ViewBuilder package to your Wisej.NET project:
dotnet add package Wisej-4-ViewBuilder
The package targets .NET Framework 4.8 and .NET 8 or later (including the -windows, Android, iOS and Mac Catalyst targets). ViewBuilder has no UI of its own. It's the static class Wisej.Web.Ext.ViewBuilder.ViewBuilder, which also adds LoadView extension methods to every ContainerControl.
Creating and loading views
| Method | What it does |
|---|---|
ViewBuilder.Create(json) | Creates a new container from the definition. The root must name a ContainerControl type with a public parameterless constructor, like Form, Page or UserControl. Overloads take a string, a Stream or an object model. |
container.LoadView(json) | Loads the definition into an existing container. The root's members are assigned to container, its controls are added after the existing ones, and the container is returned. The root doesn't need a _type. Overloads take a string, a Stream or an object model. |
Streams are read but not closed. An object model must be dynamic and support the string indexer used to read _type: a DynamicObject, or the object returned by WisejSerializer.Parse(string).
Getting started
This loads a form definition into a new form: a vertical FlexLayoutPanel holding a row of two text boxes and a button.
- C#
- VB.NET
using Wisej.Web.Ext.ViewBuilder;
var json = @"{
""_type"": ""Form"",
""text"": ""Test Form 1"",
""size"": ""500, 500"",
""headerBackColor"": ""Red"",
""padding"": ""20"",
""controls"": [
{
""_type"": ""FlexLayoutPanel"",
""name"": ""panel1"",
""layoutStyle"": ""Vertical"",
""dock"": ""Fill"",
""controls"": [
{
""_type"": ""FlexLayoutPanel"",
""layoutStyle"": ""Horizontal"",
""controls"": [
{ ""_type"": ""TextBox"", ""label.text"": ""Test"" },
{ ""_type"": ""TextBox"", ""labelText"": ""Last Name"" }
]
},
{ ""_type"": ""Button"", ""text"": ""Click Me"" }
]
}
]
}";
var form = (Form)ViewBuilder.Create(json);
form.Show();
Imports Wisej.Web.Ext.ViewBuilder
Dim json = "{
""_type"": ""Form"",
""text"": ""Test Form 1"",
""size"": ""500, 500"",
""headerBackColor"": ""Red"",
""padding"": ""20"",
""controls"": [
{
""_type"": ""FlexLayoutPanel"",
""name"": ""panel1"",
""layoutStyle"": ""Vertical"",
""dock"": ""Fill"",
""controls"": [
{
""_type"": ""FlexLayoutPanel"",
""layoutStyle"": ""Horizontal"",
""controls"": [
{ ""_type"": ""TextBox"", ""label.text"": ""Test"" },
{ ""_type"": ""TextBox"", ""labelText"": ""Last Name"" }
]
},
{ ""_type"": ""Button"", ""text"": ""Click Me"" }
]
}
]
}"
Dim form = DirectCast(ViewBuilder.Create(json), Form)
form.Show()
The two text boxes show two ways of setting a label: "label.text" walks the path to the Text of the text box's Label object, and "labelText" sets the LabelText shortcut property.
The definition format
Each JSON object describes one component:
| Member | Meaning |
|---|---|
_type | The type to create. A bare name like "TextBox" is looked up in the Wisej.Web namespace. Use the full name for anything else, e.g. "MyApp.Controls.AddressPanel". |
controls | An array of child control definitions, added to the Controls collection. |
components | (Root only.) An array of non-visual components, created first and disposed with the root container. |
| any property | Assigned to the property of the same name. Names ignore case, so labelText and LabelText both work. |
a.b.c | A property path, e.g. "label.text" or "itemTemplate.controls". |
| any event | Attaches an event handler by method name. See Event handlers. |
Values are converted with the target property's TypeConverter: "10,10" becomes a Point, "500, 500" a Size, "Top" an enum value, "Red" a Color. Arrays fill collection and list properties. A string assigned to a property that expects an object, such as dataSource or acceptButton, is taken as the name of a control or component and resolved after the whole view has loaded. So a property can refer to a control defined further down.
Components and extender properties
Non-visual components go in the root's components array. Set the properties they add to controls with componentName_PropertyName, or with a dot:
{
"controls": [
{
"_type": "TextBox",
"name": "textBox1",
"labelText": "Name:",
"toolTip1_ToolTip": "Enter the customer's name"
}
],
"components": [
{ "_type": "ToolTip", "name": "toolTip1" }
]
}
Declare components as the last member of the root object. Any root member after it is not processed.
Event handlers
A member named after an event attaches a handler. The value is a method name, resolved in this order:
- your
ResolveEventHandlercallback, if you set one; - a method declared by the root container's class, private methods included;
- a fully qualified static method, e.g.
"MyApp.Handlers.OnClick".
public partial class Form1 : Form
{
private void Form1_Load(object sender, EventArgs e)
{
this.LoadView(@"{
""controls"": [
{ ""_type"": ""TextBox"", ""name"": ""textBox1"", ""validating"": ""textBox1_Validating"" }
]
}");
}
private void textBox1_Validating(object sender, CancelEventArgs e)
{
e.Cancel = string.IsNullOrEmpty(((TextBox)sender).Text);
}
}
With Create, the root is a new instance of the _type you name, so rule 2 only finds your methods if _type is your class, e.g. "MyApp.CustomerForm" rather than "Form". Otherwise use fully qualified static methods or a custom resolver.
Handlers are never compiled from code in the definition. To support script snippets, assign a custom ResolveEventHandler that turns the string into a MethodInfo.
Data binding
A string of the form {Binding …} creates a data binding on that property instead of assigning a value:
{Binding Member, Source=name, Format=format, OnFormat=handler, OnParse=handler, SourceUpdateMode=mode, ControlUpdateMode=mode}
Everything except the member path is optional. Without Source, the binding source is the root container itself:
| Expression | Binds to |
|---|---|
{Binding} | the root container |
{Binding Customer.Name} | root.Customer.Name |
{Binding Customer.Balance, Format=c} | root.Customer.Balance, formatted as currency |
{Binding Value, Source=bs1, Format=Value: 0000} | Value of the component named bs1 |
{Binding Name, OnFormat=Name_Format, OnParse=Name_Parse} | with Format and Parse handlers attached |
- C#
- VB.NET
public partial class CustomerForm : Form
{
public Customer Customer { get; set; }
public void LoadCustomer(Customer customer)
{
this.Customer = customer;
this.LoadView(@"{
""controls"": [
{ ""_type"": ""TextBox"", ""dock"": ""Top"", ""text"": ""{Binding Customer.Name}"" },
{ ""_type"": ""TextBox"", ""dock"": ""Top"", ""readOnly"": true, ""text"": ""{Binding Customer.Balance, Format=c}"" }
]
}");
}
}
Public Class CustomerForm
Inherits Form
Public Property Customer As Customer
Public Sub LoadCustomer(customer As Customer)
Me.Customer = customer
Me.LoadView("{
""controls"": [
{ ""_type"": ""TextBox"", ""dock"": ""Top"", ""text"": ""{Binding Customer.Name}"" },
{ ""_type"": ""TextBox"", ""dock"": ""Top"", ""readOnly"": true, ""text"": ""{Binding Customer.Balance, Format=c}"" }
]
}")
End Sub
End Class
See Data Binding in the user guide for how Wisej.NET bindings work.
Example: a DataRepeater bound to a BindingSource
This definition puts a DataRepeater on a form. Its item template holds a button whose text is bound to the current item, and a BindingSource component defined in components feeds the three items.
{
"_type": "Form",
"text": "Test Form 1",
"size": "500, 500",
"headerBackColor": "Red",
"padding": "20",
"controls": [
{
"_type": "DataRepeater",
"name": "r1",
"dock": "Fill",
"itemSize": "20, 30",
"dataSource": "bs1",
"itemTemplate.controls": [
{
"_type": "Button",
"text": "{Binding Source=bs1, Format=Value: 0000}",
"click": "MyApp.Window1.button_Click"
}
]
}
],
"components": [
{ "_type": "BindingSource", "name": "bs1", "dataSource": [ 1, 2, 3 ] }
]
}
"dataSource": "bs1"is resolved to the component namedbs1after the view has loaded."itemTemplate.controls"walks into the repeater'sItemTemplateand fills itsControls."click"names a static method by its full name,MyApp.Window1.button_Click. Use your own project's namespace and class.
Customizing resolution
Two static fields on ViewBuilder let you plug in your own lookup logic. When your callback returns null, the built-in lookup runs as usual:
| Callback | Signature | Default |
|---|---|---|
ResolveReference | Func<ContainerControl, object, string, string, object> | null: names are looked up among all the child controls and components, starting from the top-level container. |
ResolveEventHandler | Func<object, EventDescriptor, string, MethodInfo> | null: methods are looked up in the root container's class, then as fully qualified static methods. |
For example, a custom ResolveEventHandler could map handler names to methods of a controller class, or compile a script into a method on the fly.
A definition can name any type with a public parameterless constructor in any loaded assembly, and attach events to static methods by name. Treat view definitions like code. Load them from your own resources, database or back end, never straight from user input. If definitions do come from outside, validate them first, and use a restrictive ResolveEventHandler.
Serializing an existing view back to JSON isn't supported yet. ViewBuilder only reads definitions.
API reference
ViewBuilder- The whole
Wisej.Web.Ext.ViewBuildernamespace