Skip to main content

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, a BindingSource or an ErrorProvider, 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.

A form titled "Test Form 1" with a red header, two labeled text boxes side by side ("Test" and "Last Name") and a full-width "Click Me" button

A form built entirely from the JSON in the Getting started example below.

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​

MethodWhat 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.

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();

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:

MemberMeaning
_typeThe 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".
controlsAn 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 propertyAssigned to the property of the same name. Names ignore case, so labelText and LabelText both work.
a.b.cA property path, e.g. "label.text" or "itemTemplate.controls".
any eventAttaches 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" }
]
}
Put components last

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:

  1. your ResolveEventHandler callback, if you set one;
  2. a method declared by the root container's class, private methods included;
  3. 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);
}
}
Handlers with Create()

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:

ExpressionBinds 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
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}"" }
]
}");
}
}

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 named bs1 after the view has loaded.
  • "itemTemplate.controls" walks into the repeater's ItemTemplate and fills its Controls.
  • "click" names a static method by its full name, MyApp.Window1.button_Click. Use your own project's namespace and class.

A form with a DataRepeater showing three rows with buttons "Value: 0001", "Value: 0002" and "Value: 0003", and an alert "I'm Wisej.Web.Button, Text: Value: 0002"

The result: one bound button per item. Clicking one runs the named handler.

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:

CallbackSignatureDefault
ResolveReferenceFunc<ContainerControl, object, string, string, object>null: names are looked up among all the child controls and components, starting from the top-level container.
ResolveEventHandlerFunc<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.

Only load definitions you trust

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.

note

Serializing an existing view back to JSON isn't supported yet. ViewBuilder only reads definitions.

API reference​