DotVVM.com Blogenhttps://www.dotvvm.com/wwwroot/Images/logos/dotvvm-tree_logo.svgDotVVM.com BlogMilan Mikušmilan.mikus@riganti.czApp Modernization - Part #4: Migrate ASCX components<p>This article is the last part of the series:</p> <ul> <li><a href="https://www.dotvvm.com/blog/104/App-Modernization-Part-1-Migrate-a-Web-Forms-app-to-DotVVM-and-NET-6">App Modernization - Part #1: Migrate Web Forms code islands to DotVVM</a></li> <li><a href="https://www.dotvvm.com/blog/105/App-Modernization-Part-2-Migrate-underscore-js-templates-and-jquery-data-loading">App Modernization - Part #2: Migrate underscore.js templates and JQuery data loading</a></li> <li><a href="https://www.dotvvm.com/blog/109/App-Modernization-Part-3-Migrate-ASCX-components">App Modernization - Part #3: Migrate ASCX components</a></li> <li><strong><a href="https://www.dotvvm.com/blog/112/App-Modernization-Part-4-Migrate-ASCX-components">App Modernization - Part #4: Migrate ASCX components</a></strong></li> </ul> <p>We will return to our fictional e-shop pages written in ASP.NET Web Forms. This article is the second of two articles migrating the <code>ProductDetail</code> page.</p> <p>We are going to migrate a bit more complex control which contains several pitfalls that you may encounter in legacy applications. We will be building on the sample from the previous article. We will also need to be familiar with <code>DotProperties</code> and DotVVM controls.</p> <p>You can follow this this migration by cloning the repo <a href="https://github.com/riganti/dotvvm-samples-webforms-advanced">https://github.com/riganti/dotvvm-samples-webforms-advanced</a>.</p> <h2 id="the-overview">The overview</h2> <p>The control is list of categories for a product detail. The control supports:</p> <ul> <li>editing all categories at once</li> <li>ordering the categories in ascending or descending order</li> <li>adding a new category</li> <li>validation of duplicates and empty entries</li> </ul> <p><img width="229" height="256" title="ASP.NET WebForms control" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="ASP.NET WebForms control" src="https://dotvvmstorage.blob.core.windows.net/blogpost/c71b3e4f-23ae-4d71-9b9b-ad10b922d139.png" border="0"></p> <p>Let's have a look at he control markup:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ProductCategories.ascx.cs" Inherits="DotVVM.Samples.Controls.ProductCategories" %&gt; &lt;div class="categories"&gt; &lt;div class="filter"&gt; &lt;%= GetSortSelect() %&gt; &lt;asp:Button UseSubmitBehavior="true" ID="SortButton" Text="Sort categories" runat="server" /&gt; &lt;/div&gt; &lt;span id="ValidationMessageSpan" class="error" runat="server"&gt;&lt;/span&gt; &lt;asp:Repeater ID="CategoryRepeater" runat="server"&gt; &lt;ItemTemplate&gt; &lt;div class="product-category &lt;%# (bool)Eval("IsError") ? "category-error": "" %&gt;"&gt; &lt;asp:HiddenField ID="CategoryIdField" runat="server" Value='&lt;%# Eval("Id") %&gt;' /&gt; &lt;asp:TextBox ID="CategoryNameField" runat="server" Text='&lt;%# Eval("Name") %&gt;' /&gt; &lt;/div&gt; &lt;/ItemTemplate&gt; &lt;/asp:Repeater&gt; &lt;div class="product-category add-category" id="AddCategoryPanel" runat="server"&gt; &lt;asp:TextBox name="newCategory" ID="NewCategoryTextBox" runat="server" CssClass="form-control" /&gt; &lt;asp:Button ID="AddButton" runat="server" CssClass="form-control" Text="Add" OnClick="AddButton_Click" /&gt; &lt;/div&gt; &lt;/div&gt; </code></pre> <p>From the first look we can see:</p> <ul> <li>Section for sorting the categories</li> <li>Repeater for rendering the inputs with category names</li> <li>Section for adding new categories</li> </ul> <p>Next let's inspect the code behind. As fields we have:</p> <pre class="brush:csharp"><code class="language-CSHARP">private readonly ProductDetailFacade _facade; private bool _sortDescending; </code></pre> <p><code>_facade</code> for loading categories from the database. <code>_sortDescending</code> stores the sorting direction during the post-back.</p> <pre class="brush:csharp"><code class="language-CSHARP">public int ProductId { get; set; } protected List&lt;Category&gt; Categories { get; private set; } = new List&lt;Category&gt;(); </code></pre> <p>We have the <code>Categories</code> property which stores the categories loaded from database or from post-back and serves as a data source for the repeater. The <code>ProductId</code> is set from parent page and contains the ID of current product.</p> <pre class="brush:csharp"><code class="language-CSHARP">public override void DataBind() { PrepareCategories(); ValidateCategories(); BindControlData(); base.DataBind(); } </code></pre> <p>The most important method for us is <code>DataBind</code> which is called from parent page. It loads the categories from the database or from post-back data using <code>PrepareCategories</code> method. Then, it validates the categories and binds the categories to the Repeater control. Even if we aim to keep changes to the backend logic to the minimum, we have to make some changes here, because in DotVVM, the data are bound to the controls automatically.</p> <pre class="brush:csharp"><code class="language-CSHARP">public List&lt;Category&gt; GetCategories() { ... } </code></pre> <p><code>GetCategories</code> is a method the parent page calls on save, to get current categories from the control to save them.</p> <pre class="brush:csharp"><code class="language-CSHARP">protected void AddButton_Click(object sender, EventArgs e) { ... } </code></pre> <p>As for methods that are used in the markup, we have the “add category” click handler. From the examples of migrations we have done before, we know that these handlers can be usually easily migrated as viewmodel methods. That is exactly what we are going to do now:</p> <pre class="brush:csharp"><code class="language-CSHARP">protected string GetSortSelect() { return GetSelectControl("categoriesDesc", _sortDescending, new SelectItem[] { new SelectItem { Text = "Ascending", Value = false }, new SelectItem { Text = "Descending", Value = true }, }); } </code></pre> <p>The last protected method is quite an example of a legacy code. Sometimes, during the migration, we would come across piece of code constricting a piece of HTML to be rendered in the page. Such controls constructed on in the code-behind were used for various reasons. Often, links with query parameters in the <code>href</code>, headers for table columns, or a common example of that is a generated select list. This method renders the <code>select</code> tag with options as a string to render in the page.</p> <p>We do not need to go into the private methods for now, a brief overview should be enough to inform our strategy for migrating the control. As there is quite lot of logic, simply using DotVVM control properties as before will not be enough here. Controls like this one need the viewmodel. Having a dedicated viewmodel that would contain the data and the logic of our control is often the best way to go.</p> <h2 id="scaffolding">Scaffolding</h2> <p>We can now do the basic setup for the new control with viewmodel. We create new DotVVM markup control <code>ProductCategories.dotcontrol</code> in its own folder called <code>ProductCategories</code>. In the same folder, we create a viewmodel for the control: <code>ProductCategoriesViewModel</code>. The viewmodel will contain the logic migrated from the originals ASCX control code-behind. In this case, there is no need to create DotVVM code-behind class for <code>ProductCategories.dotcontrol</code> control. Instead of passing the data to the control using DotVVM properties, you can pass everything through the viewmodel.</p> <pre class="brush:csharp"><code class="language-CSHARP">@viewModel DotVVM.Samples.Migrated.Controls.ProductCategories.ProductCategoriesViewModel &lt;div class="categories"&gt; &lt;/div&gt; </code></pre> <p>We should have a skeleton control file like this, an empty class for the viewmodel, and we must not forget to register the control in the <code>DotvvmStartup.RegisterControls</code> method:</p> <pre class="brush:csharp"><code class="language-CSHARP">config.Markup.AddMarkupControl( "cc", "ProductCategories", "Migrated/Controls/ProductCategories/ProductCategories.dotcontrol"); </code></pre> <p>Now, we can add the control to the parent <code>ProductDetail</code> page. The <code>ProductCategoriesViewModel</code> must be nested inside of the parent page, so we add a property <code>CategoriesModel</code> to <code>ProductDetailViewModel</code>. That is because <code>ProductDetailViewModel</code> is the viewmodel of the parent page from which <code>ProductCategories</code> control is referenced:</p> <pre class="brush:csharp"><code class="language-CSHARP">public class ProductDetailViewModel : SiteViewModel { public int ProductId { get; set; } public List&lt;Tag&gt; Tags { get; set; } public ProductCategoriesViewModel CategoriesModel { get; set; } // ... } </code></pre> <p>Note that we have <code>ProductId</code> and <code>Tags</code> properties from previous migration of <code>ProductTags</code> control. Now that we have created a property of <code>ProductCategoriesViewModel</code>, we can add the <code>ProductCategories</code> control into the markup of <code>ProductDetail</code> page and bind it like so:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;cc:ProductCategories DataContext={value: CategoriesModel} /&gt; </code></pre> <p>Notice that the data context in which <code>ProductCategories</code> control is used must match the viewmodel defined by <code>@viewmodel</code> directive in the <code>ProductCategories.dotcontrol</code> markup file.</p> <p>To change the data context for the control tag in <code>ProductDetail</code> page markup, we can use the <code>DataContext</code> property and bind it to the property in the viewmodel of the correct type. In this case, it’s the <code>CategoriesModel</code> property of <code>ProductDetailViewModel</code>.</p> <h2 id="migrating-the-sorting">Migrating the sorting</h2> <p>Now that we have basic scaffolding in place, we can start migrating the markup and the backend. Let's take a look at the sort direction ComboBox control:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;div class="filter"&gt; &lt;%= GetSortSelect() %&gt; &lt;asp:Button UseSubmitBehavior="true" ID="SortButton" Text="Sort categories" runat="server" /&gt; &lt;/div&gt; </code></pre> <p>We have the <code>GetSortSelect</code> method which renders the select with options based on the provided <code>SelectItem</code>s. The control <code>dot:ComboBox</code> helps us here, but we still need to have a look how the selected value is used co we can replicate it in our DotVVM control. In the ASPX code behind, there is a private field <code>_sortDescending</code>. After searching for its references, we can see:</p> <pre class="brush:csharp"><code class="language-CSHARP">return GetSelectControl("categoriesDesc", _sortDescending, new SelectItem[] { new SelectItem { Text = "Ascending", Value = false }, new SelectItem { Text = "Descending", Value = true }, }); </code></pre> <p>This code selects the value in the control on the post-back, but the <code>dot:ComboBox</code> control does this by default. We can take the <code>SelectItem</code> array and move it to <code>ProductCategoriesViewModel</code>. We can also see that we need to create property <code>SortDescending</code> in the viewmodel to hold the selected value of the combobox.</p> <pre class="brush:csharp"><code class="language-CSHARP">public class ProductCategoriesViewModel : SiteViewModel { public bool SortDescending { get; set; } public SelectItem[] SortingOptions { get; } = new SelectItem[] { new SelectItem { Text = "Ascending", Value = false }, new SelectItem { Text = "Descending", Value = true }, }; } </code></pre> <p><code>SortingOptions</code> property has only getter because we don't want to make changes to the sorting options on client side and send them back to server during post-back.</p> <p>Next reference in the original ASCX control code behind is <code>_sortDescending = HttpContext.Current.GetBoolQuery("categoriesDesc");</code> This is just a way to get value of the sorting direction during ASP.NET Web Forms post-back, DotVVM does this by default when deserializing viewmodel on server side.</p> <p>Other references are just usages, those are simple to deal with, when we copy the logic over we bulk replace <code>_sortDescending</code> to <code>SortDescending</code> in the DotVVM <code>ProductCategories</code> control.</p> <p>Now we migrate the original ASCX markup:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;div class="filter"&gt; &lt;%= GetSortSelect() %&gt; &lt;asp:Button UseSubmitBehavior="true" ID="SortButton" Text="Sort categories" runat="server" /&gt; &lt;/div&gt; </code></pre> <p>To DotVVM markup like this:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;form class="filter"&gt; &lt;dot:ComboBox DataSource={value: SortingOptions} SelectedValue={value: SortDescending} ItemTextBinding={value: Text} ItemValueBinding={value: Value == true} /&gt; &lt;dot:Button ID="SortButton" Click={command: null} Text="Sort categories" IsSubmitButton=true /&gt; &lt;/form&gt; </code></pre> <p><code>SortingOptions</code> and <code>SortDescending</code> properties are bound to <code>DataSource</code> and <code>SelectedValue</code> respectively. It is the collection of options and the selected value. Data context for <code>ItemTextBinding</code> and <code>ItemValueBinding</code> properties is an item from the collection bound to the <code>DataSource</code> property, in this case the class <code>SelectItem</code>. We use <code>ItemTextBinding</code> and <code>ItemValueBinding</code> DotProperties to select members of the class <code>SelectItem</code> which should be used as a value and a text description for the combobox items.</p> <p>Now to the little hack I made here <code>{value: Value == true}</code>: The issue is that <code>ItemValueBinding</code> control property only supports primitive types. However, we have <code>object</code> as a type of our <code>Value</code> here because in the legacy system the implementation was not using generics. The syntax of data-bindings in DotVVM does not support casting, but since we know that only <code>true</code> and <code>false</code> values appear there, comparing to <code>true</code> is safe enough. This is unfortunate, but there is a better solution than rewriting every single reference to the <code>SelectItem.Value</code> in the code-behind. </p> <p>The binding of the sort button <code>Click={command: null}</code> may seem strange. What it does is it invokes the DotVVM post-back without calling any command in the viewmodel. Since we are refreshing the data on every postback, there is no need to call any command. Notice also the <code>IsSubmitButton=true</code> property which tells DotVVM to render the button as a submit button. When such button is used in a <code>form</code> element, it will be activated automatically when the user presses the Enter key.</p> <h2 id="migrating-the-categories-repeater">Migrating the categories repeater</h2> <h3 id="migrating-the-code-behind">Migrating the code-behind</h3> <p>Migrating the main Repeater and logic for categories is the main challenge for us. In the legacy applications, it is usually the main grid or repeater with the main collection that has the most logic on the back-end surrounding it. In cases such as these, especially if the logic gets complicated, it would be very time consuming to rewrite everything perfectly into for our viewmodel.</p> <p>Instead, we can copy the logic over, and touch only the parts that we absolutely need to change. So the two methods from <code>ProductCategories.ascx.cs</code> that we can just copy over to our DotVVM viewmodel are:</p> <pre class="brush:csharp"><code class="language-CSHARP">public override void DataBind() { PrepareCategories(); ValidateCategories(); BindControlData(); base.DataBind(); } public List&lt;Category&gt; GetCategories() { return Categories.OrderBy(c =&gt; c.Id).ToList(); } </code></pre> <p>For the <code>DataBind</code> method, we have to get rid of the <code>override</code> keyword and delete <code>base.DataBind();</code>.</p> <p>Both methods are called from the parent page. Here we are lucky that the reading data from controls, validating, and binding data back to the controls are nicely split apart. We may not be so lucky on some legacy projects. However, the parts that need to be changed are usually semantically the same, however deep in the code they are.</p> <p>Before anything else, we need to bring over our <code>Categories</code> property from <code>ProductCategories.ascx.cs</code>:</p> <pre class="brush:csharp"><code class="language-CSHARP">protected List&lt;Category&gt; Categories { get; private set; } = new List&lt;Category&gt;(); </code></pre> <p>We need to make changes as we include it to the <code>ProductDetailViewModel</code>:</p> <pre class="brush:csharp"><code class="language-CSHARP">public List&lt;Category&gt; Categories { get; set; } = new List&lt;Category&gt;(); </code></pre> <p>It needs to be a public property with both getter and setter public to have the DotVVM fill it on post-back.</p> <p>From the <code>DataBind</code> method, the <code>ValidateCategories</code> method we can simply copy over from <code>ProductCategories.ascx.cs</code> into <code>ProductCategoriesViewModel</code>.</p> <p>When we look at <code>BindControlData</code> in the ASCX code behind we see:</p> <pre class="brush:csharp"><code class="language-CSHARP">private void BindControlData() { CategoryRepeater.DataSource = Categories; CategoryRepeater.DataBind(); if (Categories.Any(c =&gt; c.IsError)) { ValidationMessageSpan.Visible = true; ValidationMessageSpan.InnerText = "Some categories are invalid"; } } </code></pre> <p>We can see something like this in many legacy Web Forms applications. Here we bind the categories to the Repeater, and the eventual validation message to the span control. In DotVVM, the <code>dot:Repeater</code> is bound in the markup and does not need any work done in the code-behind. So we do not need first 2 lines of the <code>BindControlData</code> method.</p> <p>However, we do need a property to hold the validation message. In DotVVM we do not reference controls on the page from the code-behind. Instead we create property to hold a validation message and then bind the property in the DotVVM markup to the span <code>InnerText</code> property.</p> <pre class="brush:csharp"><code class="language-CSHARP">public string ValidationMessageSpanText { get; private set; } </code></pre> <p>So, we created the property in <code>ProductCategoriesViewModel</code>. The property has a private setter because we do not need it to be transferred from client side to server side. Now the migrated <code>BindControlData</code> method will look like this:</p> <pre class="brush:csharp"><code class="language-CSHARP">private void BindControlData() { if (Categories.Any(c =&gt; c.IsError)) { ValidationMessageSpanText = "Some categories are invalid"; } } </code></pre> <p>Generally, when migrating parts of Web Forms code-behind that bind data to the controls on page these rules apply:</p> <ul> <li>Setting <code>DataSource</code> for repeaters, grids, item lists and other can be discarded from code-behind and moved to DotVVM markup</li> <li>For <code>Visible</code> properties we can use <code>IncludeInPage</code> DotProperty in the DotVVM markup, define property to store the visibility value in the viewmodel for complex cases.</li> <li>For setting values and texts: if there already is a property we can just go ahead and set the property. This is usually the case for text boxes, checkboxes, combo boxes and other such controls.</li> <li>For text on spans, labels and similar we need to create a property in the viewmodel and bind it to the <code>InnerText</code> property of the corresponding control tag in the DotVVM markup.</li> </ul> <p>The last on our list is the <code>PrepareCategories</code> method where data is loaded into the <code>Categories</code>.</p> <pre class="brush:csharp"><code class="language-CSHARP">private void PrepareCategories() { if (!IsPostBack) { Categories = ProductId &gt; 0 ? _facade.GetCategories(ProductId) : new List&lt;Category&gt;(); } else { Categories = ReadCategories().ToList(); } _sortDescending = HttpContext.Current.GetBoolQuery("categoriesDesc"); Categories = _sortDescending ? Categories.OrderByDescending(x =&gt; x.Name).ToList() : Categories.OrderBy(x =&gt; x.Name).ToList(); } </code></pre> <p>On first load of the page, the categories are loaded from the database. On post-back, they are read from the view-state. The <code>ReadCategories</code> used here just reads the <code>Id</code>s and <code>Name</code>s from <code>asp:TextBox</code> and <code>asp:HiddenField</code> in the <code>asp:Repeater</code>. It parses the <code>Id</code> as int value and creates a Category object for each repeater item.</p> <p>DotVVM framework transfers the data on post-back and deserializes them into the viewmodel automatically. Therefore in these Web Forms application, when we see a code that parses data from view-state or query parameters, we can usually get rid of it during a migration to DotVVM.</p> <p>In our case, we can safely throw away whole <code>else</code> branch.</p> <p>Likewise, we can discard the line where <code>_sortDescending</code> is being loaded from query. We have already dealt with sorting before and now we have <code>SortDescending</code> property in our viewmodel that holds the sorting direction for us. Only change we need to make is replace <code>_sortDescending</code> to <code>SortDescending</code> for sorting categories.</p> <p>We need to change <code>!IsPostBack</code> into <code>!Context.IsPostBack</code>. We have <code>Context</code> property in our viewmodel as it extends <code>DotvvmViewModelBase</code>. The <code>Context</code> property is injected automatically by DotVVM. From DotVVM version 4, the <code>Context</code> property is injected automatically as long as the viewmodel instance is created before the <code>Init</code> stage of page lifecycle.</p> <p>For us it means we need to create instance of <code>ProductCategoriesViewModel</code> in the constructor of <code>ProductDetailViewModel</code></p> <p>The resulting migrated method looks like this:</p> <pre class="brush:csharp"><code class="language-CSHARP">private void PrepareCategories() { if (!Context.IsPostBack) { Categories = ProductId &gt; 0 ? _facade.GetCategories(ProductId) : new List&lt;Category&gt;(); } Categories = SortDescending ? Categories.OrderByDescending(x =&gt; x.Name).ToList() : Categories.OrderBy(x =&gt; x.Name).ToList(); } </code></pre> <p>We have still some work to do. We need to create property <code>ProductId</code> that will get its value from the query parameter <code>productId</code>. In DotVVM this is easy. We just add the property to our viewmodel and decorate it with <code>FromQuery</code> attribute. If you are using MVC in your application, make sure you reference <code>FromQuery</code> attribute from the DotVVM namespace.</p> <pre class="brush:csharp"><code class="language-CSHARP">public class ProductCategoriesViewModel : SiteViewModel { // ... [FromQuery("productId")] public int ProductId { get; set; } // ... } </code></pre> <p>The life-cycle requirements is the same as with <code>Context</code> property. As long as the viewmodel instance exists in the root viewmodel (in our case <code>ProductDetailViewModel</code>) before the <code>Init</code> phase of the page, the <code>ProductId</code> property will be filled by DotVVM with the value from the URL query string.</p> <p>The last unknown identifier in our migrated <code>PrepareCategories</code> method is <code>_facade</code>. Depending on the application, we can use dependency injection, or in our case we can just create the instance in the <code>ProductDetailViewModel()</code> constructor. Because it is a service dependency and we do not want DotVVM to serialize it, we make it a <code>private</code> <code>readonly</code> field.</p> <pre class="brush:csharp"><code class="language-CSHARP">public ProductCategoriesViewModel() { _facade = new ProductDetailFacade(); } </code></pre> <p>All the code-behind logic for loading categories is now migrated, we can migrate the markup.</p> <h3 id="migrating-the-control-markup">Migrating the control markup</h3> <p>We can copy over the the <code>&lt;asp:Repeater ... &gt;</code> from <code>ProductCategories.ascx</code> into the <code>ProductCategories.dotcontrol</code>. We change change all the <code>asp</code> prefixes for <code>dot</code>. We delete all the <code>runat="server"</code> attributes. Also we do not need the <code>asp:HiddenField</code> because DotVVM takes care of our page data. We should have something like this:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;dot:Repeater ID="CategoryRepeater"&gt; &lt;ItemTemplate&gt; &lt;div class="product-category &lt;%# (bool)Eval(" IsError") ? "category-error" : "" %&gt;"&gt; &lt;dot:TextBox ID="CategoryNameField" Text='&lt;%# Eval("Name") %&gt;' /&gt; &lt;/div&gt; &lt;/ItemTemplate&gt; &lt;/dot:Repeater&gt; </code></pre> <p>Next we change Web Forms code islands into bindings. <code>'&lt;%# Eval("Name") %&gt;'</code> becomes <code>{value: Name}</code>. For conditionally adding CSS class we can use DotVVM property group <code>Class-*</code>. The <code>product-category</code> class will be present always so we put in <code>Class-product-category="true"</code>. The <code>category-error</code> we put in binding: <code>Class-category-error={value: IsError}</code>. Based on these group properties, DotVVM will construct and update the <code>class</code> attribute as needed.</p> <p>The Repeater needs a data source so we create a <code>DataSource</code> binding and bind it to <code>Categories</code> collection.</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;dot:Repeater ID="CategoryRepeater" DataSource={value: Categories}&gt; &lt;ItemTemplate&gt; &lt;div Class-product-category="true" Class-category-error={value: IsError}&gt; &lt;dot:TextBox ID="CategoryNameField" Text={value: Name}/&gt; &lt;/div&gt; &lt;/ItemTemplate&gt; &lt;/dot:Repeater&gt; </code></pre> <p>Not to forget the validation message. We add a <code>&lt;span&gt;</code> and add <code>IncludeInPage</code> property. DotVVM value bindings support <code>string.IsNullOrEmpty()</code> method so we can use it to check when to display the span. We also add <code>InnerText</code> binding on the span as discussed earlier.</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;span IncludeInPage={value: string.IsNullOrEmpty(ValidationMessageSpanText)} InnerText={value: ValidationMessageSpanText} /&gt; </code></pre> <p>Now the migration of category Repeater logic is complete. To make the control work within the page, we have to call <code>ProductCategoriesViewModel.BindData()</code> in the <code>ProductDetailViewModel</code>. The <code>BindData()</code> is designed to be called by a parent page.</p> <pre class="brush:csharp"><code class="language-CSHARP">public class ProductDetailViewModel : SiteViewModel { // ... public override Task Load() { //... Categories.DataBind(); return base.Load(); } } </code></pre> <p>Notice we are using <code>Load()</code> method here. <code>Load()</code> is called before commands from command bindings are invoked. Calling <code>DataBind</code> here ensures categories are validated and ready when eventually <code>Save()</code>, <code>Add()</code> or other commands are invoked.</p> <h2 id="migrating-add-new-category">Migrating Add new category</h2> <p>Taking look at the "Add Category" section, the last section we have to migrate:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;div class="product-category add-category" id="AddCategoryPanel" runat="server"&gt; &lt;asp:TextBox name="newCategory" ID="NewCategoryTextBox" runat="server" CssClass="form-control" /&gt; &lt;asp:Button ID="AddButton" runat="server" CssClass="form-control" Text="Add" OnClick="AddButton_Click" /&gt; &lt;/div&gt; </code></pre> <p>We change <code>asp</code> prefixes into <code>dot</code> prefixes. We remove all <code>runat</code> attributes. We change the <code>CssClass</code> to ordinary <code>class</code> attribute. We add a <code>Text</code> property for the <code>dot:TextBox</code> with a new value binding.</p> <p>We need to create a property for the textbox text in the viewmodel. When naming the property, we can take inspiration from the textbox ID and we can call it <code>NewCategoryTextBoxText</code>.</p> <p>For the <code>dot:Button</code> we change the <code>OnClick</code> to <code>Click</code> and add a new command binding. Taking inspiration from the ID we name the command <code>AddButtonClick</code>.</p> <p>The migrated section should look something like this:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;div class="product-category add-category" id="AddCategoryPanel"&gt; &lt;dot:TextBox name="newCategory" ID="NewCategoryTextBox" class="form-control" Text={value: NewCategoryTextBoxText} /&gt; &lt;dot:Button ID="AddButton" class="form-control" Text="Add" Click={command: AddButtonClick()} /&gt; &lt;/div&gt; </code></pre> <p>Of course, the property <code>NewCategoryTextBoxText</code> and the method <code>AddButtonClick()</code> do not exist in the viewmodel at this point. With the commercial version of <a href="https://www.dotvvm.com/products/visual-studio-extensions">DotVVM for Visual Studio</a>, we can place the caret over the symbols and use <code>CTRL+.</code> to shows actions to create a new text property and a new method respectively. Otherwise, we can just create them ourselves in our viewmodel.</p> <p>The logic for adding the category in the backend will stay very similar. Let's have a look:</p> <pre class="brush:csharp"><code class="language-CSHARP">protected void AddButton_Click(object sender, EventArgs e) { Categories.Add(new Category { Id = Categories.Count + 1, Name = NewCategoryTextBox.Text }); NewCategoryTextBox.Text = ""; ValidateCategories(); BindControlData(); } </code></pre> <p>We can copy the content of the <code>AddButton_Click</code> from the <code>ProductCategories.ascx.cs</code> file into our newly created <code>AddButtonClick</code> of <code>ProductCategoriesViewModel</code>.</p> <p>The only error that shows up is that <code>NewCategoryTextBox</code> does not exist. The cure for this error is simple - we have already prepared <code>NewCategoryTextBoxText</code> property. We can just change <code>NewCategoryTextBox.Text</code> into <code>NewCategoryTextBoxText</code> and everything should work.</p> <p>The result is pretty similar to what it was in the <code>ProductCategories.ascx.cs</code> backend.</p> <pre class="brush:csharp"><code class="language-CSHARP">public void AddButtonClick() { Categories.Add(new Category { Id = Categories.Count + 1, Name = NewCategoryTextBoxText }); NewCategoryTextBoxText = ""; ValidateCategories(); BindControlData(); } </code></pre> <p>The control <code>ProductCategories</code> is now migrated to DotVVM.</p> <h2 id="productdetail-page">ProductDetail page</h2> <p>To finish the <code>ProductDetail</code> page, we need to migrate the save functionality from <code>ProductDetail.ascx</code>. The relevant part of the ASPX markup is:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;asp:Button UseSubmitBehavior="true" runat="server" Text="Save" OnClick="Save_Click" /&gt; </code></pre> <p>We migrate the "Save" button same way we migrated the buttons before and place it into the <code>ProductDetail.dothtml</code>:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;dot:Button Text="Save" Click={command: Save()} /&gt; </code></pre> <p>We create <code>Save()</code> method in the <code>ProductDetailViewModel</code>. Then we copy the content of the <code>Save_Click</code> method from <code>ProductDetail.aspx.cs</code> over:</p> <pre class="brush:csharp"><code class="language-CSHARP">public void Save() { var categories = ProductCategoriesControl.GetCategories(); if (!categories.Any(c =&gt; c.IsError)) { _facade.SaveCategories(_productId, categories); } else { Message = "Cannot save."; } } </code></pre> <p>We have no <code>ProductCategoriesControl</code> in our viewmodel, but we do have <code>Categories</code> property with the viewmodel of our newly migrated <code>ProductCategories</code> control. So we change <code>ProductCategoriesControl</code> to <code>Categories</code>.</p> <p>We also do not have <code>_productId</code>, but we do have <code>ProductId</code> in <code>ProductDetailViewModel</code>. We can change that.</p> <p>We have already created the <code>Message</code> property when we migrated the <code>Tags</code> control, so the message should work out of the box.</p> <p>Migrated <code>Save</code> method is now finished and looks like this:</p> <pre class="brush:csharp"><code class="language-CSHARP">public void Save() { var categories = Categories.GetCategories(); if (!categories.Any(c =&gt; c.IsError)) { _facade.SaveCategories(ProductId, categories); } else { Message = "Cannot save."; } } </code></pre> <p>Notice here that the usage of <code>GetCategories()</code> method that was defined on the original <code>ProductCategories.ascx</code> control. We have migrated the method to the viewmodel of <code>ProductCategories</code> DotVVM control, because the viewmodel is what holds the data in DotVVM.</p> <p>Another important side note: the property <code>ProductId</code> we are using in this save method is decorated with <code>[FromQuery("productId")]</code> attribute. By using the attribute DotVVM knows to fill <code>ProductId</code> with the value from the query string.</p> <h2 id="conclusion">Conclusion</h2> <p>This concludes the migration of the <code>ProductCategories</code> and the associated <code>ProductDetail</code> page. We have explored possibility of migrating complex logic in control code-behind into DotVVM control with the least amount of changes to the logic itself.</p> <p>We have found that the best way to do it is to introduce a viewmodel with serves as the data context for the migrated control and also holds the logic migrated from ASCX control code behind.</p> <p>We have also shown how to integrated the control viewmodel into the parent page.</p>Tue, 12 Dec 2023 09:33:54 ZTomáš Hercegtomas.herceg@riganti.czReleased DotVVM 4.2<p>We are happy to <strong>announce the release of DotVVM 4.2</strong> which brings many important improvements, and <strong>prepares the ground for DotVVM 5.0</strong> which we’ve already started working on.</p><h2>New features in the framework</h2><p>DotVVM 4.2 brings several larger features:</p><ul><li><strong>Validation in static commands </strong>– you can now enable validation for static commands and use validation attributes or <em>IValidatableObject</em> on arguments of the called method. Also, you can build and return your own model state which allows you to implement more complex validation logic. We’ve described it in a greater detail in the <a href="https://www.dotvvm.com/blog/108/Announcing-Preview-7-of-DotVVM-4-2-0">previous blog post</a> and in an <a href="https://forum.dotvvm.com/t/4-2-preview-validation-in-staticcommands-and-more/43">announcement on the community forum</a>.</li><li><strong>Custom primitive types </strong>– this feature helps you with using custom types which are treated as primitive values and can thus be used in URL parameters, <em>SelectedValue</em> of <em>ComboBox</em> control, and other places. This is useful to implement for example <a href="https://en.wikipedia.org/wiki/Strongly_typed_identifier">strongly-typed identifiers</a> which are popular anywhere Domain-Driven Design architecture is used. We also described the feature in detail in the <a href="https://www.dotvvm.com/blog/108/Announcing-Preview-7-of-DotVVM-4-2-0">blog post about DotVVM 4.2 Preview 7</a>.</li><li><strong>ASP.NET Web Forms adapters </strong>is a simple package which helps in the <a href="https://www.dotvvm.com/modernize">scenario of modernization of legacy ASP.NET applications using DotVVM</a>. When you are in the process of migration, some pages are still using ASP.NET Web Forms while the others are already migrated into DotVVM. However, in such case you cannot use the classic RouteLink control because it may point to pages which haven’t been migrated yet. The <em>DotVVM.Adapters.WebForms </em>package contains the <em>HybridRouteLink</em> control which is extending the built-in <em>RouteLink </em>with a support to point to Web Forms routes. You can use this control at the migration time, and replace it with the classic <em>RouteLink </em>after the migration is finished.</li><li><strong>Simplified JS API to manipulate with state</strong> – this is a set of functions that help reading and modifying the viewmodel from JS code. See the <a href="https://forum.dotvvm.com/t/4-2-preview-simplified-view-model-api-in-js-modules/65">announcement on the community forum</a> for more info.</li><li><strong>Support for Knockout deferred updates</strong> – in order to increase the client-side performance of more complex DotVVM pages, some users were turning on the <a href="https://knockoutjs.com/documentation/deferred-updates.html">Knockout’s deferred update</a> functionality. While this functionality worked correctly in most cases (we only found some Business Pack with slight issues), this wasn’t officially supported and tested. Now we run all tests in both normal and deferred updates mode, and you can turn this mode on using <em>config.ExperimentalFeatures.KnockoutDeferUpdates</em> feature flag.</li><li><strong>Metrics and Prometheus support</strong> – DotVVM now publishes a plenty of useful metrics including the size of the viewmodel, time needed for the serialization or execution of the commands, server-side viewmodel cache, and more. </li></ul><p>Of course, there are more smaller changes and bug fixes. You can find a complete list in the <a href="https://github.com/riganti/dotvvm/releases/tag/v4.2.0">Release notes</a>.</p><h2>Bootstrap for DotVVM</h2><p><strong>The packages for Bootstrap 3, 4, and 5, support DotVVM 4.2 from the first day</strong>. If you don’t see this version on your <a href="https://www.dotvvm.com/docs/4.0/pages/dotvvm-for-visual-studio/dotvvm-private-nuget-feed">DotVVM Private NuGet feed</a>, make sure you have an <a href="https://www.dotvvm.com/products/bootstrap-for-dotvvm">active license</a> (each license includes 12 months of upgrades and bug fixes since the day of purchase).</p><p>We’d like to thank the community for feedback, especially to the new <strong>Bootstrap 5 </strong>package which was completely rewritten using the new <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/control-development/composite-controls">Composite Controls</a> approach. We fixed 15 issues in various controls, and added several features suggested by the users. Please keep the feedback coming.</p><p>We are still aware of several issues in the DotVVM for Visual Studio extension and we are preparing a fix.</p><h2>DotVVM Business Pack</h2><p>Except for numerous bug fixes, there were also several enhancements to <a href="https://www.dotvvm.com/products/dotvvm-business-pack"><strong>DotVVM Business Pack</strong></a>.</p><ul><li><strong><em>DotVVM.BusinessPack.Messaging</em> package </strong>provides a simple way to connect the DotVVM page to a SignalR Core hub and invoke commands in the page from the server. This helps adding nice real-time capabilities in the pages. We’ve created a <a href="https://www.youtube.com/watch?v=CEdMa6Hq8fc">video about making UI for long-running operations</a> and creating a <a href="https://www.youtube.com/watch?v=jc3tDpTf3hU">simple interface for ChatGPT</a> using DotVVM Business Pack Messaging.</li><li><strong>Support for </strong><a href="https://www.dotvvm.com/docs/4.0/pages/concepts/auto-ui/overview"><strong>Auto UI</strong></a> enhances the feature of DotVVM 4.1 with a support for Business Pack controls. When you use a <em>DateTime</em> field in the auto-generated interface, a Business Pack <em>DatePicker, TimePicker </em>or <em>DateTimePicker </em>control will be used based on the given property metadata. Similarly, multi-select experience is done via the <em>MultiSelect </em>control, and so on. We’ll be happy for your feedback in order to add features to this integration.</li><li><strong>Support for CSS layers </strong>simplifies the life for front-end developers by wrapping the Business Pack styles in a separate <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@layer">CSS layer</a>. Thanks to this, it is easier to override Business Pack styles.</li><li><strong>Support for bundled scripts and styles</strong> – beginning with DotVVM Business Pack 4.2, you can choose whether you want to reference chunked resources or a complete bundle in your pages. With chunking enabled, DotVVM loads just the scripts and styles for the Business Pack components that were actually used on the page. This can save some bytes, but if your pages contain many components, it is more efficient to load the entire Business Pack bundle because of caching. Chunking is typically preferred on public-facing sites or on the sites where the speed of the first page load is crucial. Now you can choose between these two modes.</li><li><strong>GridView filter localization </strong>– we added an option to localize filter operators (equals, starts with, and so on) using server-side styles. We are working on supporting <em>IStringLocalizer</em> which will be even easier.</li><li><strong>GridView filters support simple expressions in column values</strong> – if you bind a column not to just a property but to a simple transform which DotVVM can evaluate in both ways, e. g. <em>someDate.ToBrowserLocalTime()</em>, it will work in <em>GridView </em>filters. This is useful when you store data in UTC but want to display them in user’s local time and apply this to the filters also.</li><li><strong>TreeView performance – </strong>we added an option to not expand child items on selecting them, and we’ve rewritten some parts of the component so when you expand a large subtree, it is now much faster.</li></ul><p>We have also <strong>implemented editing support for GridView </strong>which can switch the control in Excel-like editor where the user can edit any cell, however this feature didn’t make it in the 4.2 release.</p><p>We also have a <strong>working prototype of grouping for GridView</strong>, however we are still working on the API of this feature so it would be more easy to use, especially with popular ORMs like Entity Framework.</p><h2>Plans for DotVVM 5</h2><p>The main feature of DotVVM 5 will be <strong>extensible GridViewDataSet</strong>. </p><p>Currently, the data sets support sorting and paging (filtering is added only in Business Pack via inheritance, which is not a perfect solution as it is hard to combine with other features). </p><ul><li>The default paging supports only one mode (<em>PageIndex </em>and <em>PageSize </em>properties) which is not always easy to implement, especially with data sources which use token-based paging or any other method of paging. The new paging will be extensible using generics, so you will be able to plug your own implementation of paging. To make it work with the default <em>DataPager </em>control, the paging will need to declare capabilities (can go to the previous page, can go to a page with a specified index, knows the total number of pages, and so on). Based on that, the <em>DataPager </em>will render just the controls which make sense for the concrete data set. In the simplest case when the paging can only go forward, there will be only the <em>Next </em>button.</li><li>The default sorting supports only one column. Again, we want to make it pluggable so you will be able to use multi-criteria sorting and implement your own strategy for the user to define which columns will be used in which order.</li><li>There will be a pluggable filtering. In the framework, the filtering options will be just an empty placeholder, but we will be able to plug the Business Pack implementation of filters without using the inheritance. </li></ul><p>The <em>GridView </em>and <em>DataPager </em>will also get a new event called <em>LoadData </em>which can be used to refresh the data using a static command. This will allow sorting, paging and filtering without posting the entire viewmodel back to the server.</p><p>We also plan to introduce a new pager controls, for example the <em>AppendableDataPager </em>which will make the “infinite” scrolling experience.</p><h2>Watch us at .NET Conf!</h2><p><a href="https://www.dotnetconf.net/"><strong>.NET Conf 2023</strong></a> is almost here – it starts on <strong>Tuesday, November 14</strong>. This year we will have a session about <strong>modernization of legacy ASP.NET apps using DotVVM by Tomas Herceg </strong>– it’s on <strong>Thursday, November 16, 9:00 – 9:30 AM UTC</strong> (1:00 – 1:30 AM Pacific Time). The session will be recorded so even if you cannot join us at this time, you will be able to watch it on demand.</p><p><img width="640" height="312" title="Two ways of migrating old ASP.NET web apps to .NET 7/8" alt="Two ways of migrating old ASP.NET web apps to .NET 7/8" src="https://dotvvmstorage.blob.core.windows.net/blogpost/f81ab8b1-6454-40dc-b807-0542833bc14f.png" border="0"><p><br><p>As always, we will be happy to hear your feedback. <strong>Please tell us about your experience with upgrading to DotVVM 4.2 on our </strong><a href="https://forum.dotvvm.com"><strong>new community forum</strong></a><strong>.</strong>Sun, 12 Nov 2023 16:21:37 ZTomáš Hercegtomas.herceg@riganti.czJoin our DotVVM session at .NET Conf 2023<p><strong><a href="https://www.dotnetconf.net/"><strong>.NET Conf</strong></a></strong> is a free, three-day, virtual developer event that celebrates the major releases of the <a href="https://www.dot.net/">.NET development platform</a>. It is co-organized by the .NET community and Microsoft, and sponsored by the .NET Foundation and our ecosystem partners. Come celebrate and learn about <strong>what you can do with .NET 8</strong>.<p>Same as last year, <strong>DotVVM is one of the .NET Conf sponsors</strong>.<p><a href="https://www.dotnetconf.net/"><img width="640" height="360" title=".NET Conf - November 14" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt=".NET Conf - November 14" src="https://dotvvmstorage.blob.core.windows.net/blogpost/fb28fb08-2703-410d-a047-f91f7149cd91.png" border="0"></a><p><br><p>This year we managed to have a session about <strong>modernization of legacy ASP.NET apps using DotVVM by Tomas Herceg </strong>– it’s on <strong>Thursday, November 16, 9:00 – 9:30 AM UTC</strong> (1:00 – 1:30 AM Pacific Time).<p><img width="640" height="312" title="Two ways of migrating old ASP.NET web apps to .NET 7/8" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="Two ways of migrating old ASP.NET web apps to .NET 7/8" src="https://dotvvmstorage.blob.core.windows.net/blogpost/f81ab8b1-6454-40dc-b807-0542833bc14f.png" border="0">Fri, 20 Oct 2023 12:52:56 ZMilan Mikušmilan.mikus@riganti.czApp Modernization - Part #3: Migrate ASCX components<p>This article is the third part of the series:</p> <ul> <li><a href="https://www.dotvvm.com/blog/104/App-Modernization-Part-1-Migrate-a-Web-Forms-app-to-DotVVM-and-NET-6">App Modernization - Part #1: Migrate Web Forms code islands to DotVVM</a></li> <li><a href="https://www.dotvvm.com/blog/105/App-Modernization-Part-2-Migrate-underscore-js-templates-and-jquery-data-loading">App Modernization - Part #2: Migrate underscore.js templates and JQuery data loading</a></li> <li><strong><a href="https://www.dotvvm.com/blog/109/App-Modernization-Part-3-Migrate-ASCX-components">App Modernization - Part #3: Migrate ASCX components</a></strong></li> <li><a href="https://www.dotvvm.com/blog/112/App-Modernization-Part-4-Migrate-ASCX-components">App Modernization - Part #4: Migrate ASCX components</a></li> </ul> <p>In this example, we will return to our fictional customer internal e-shop pages written in ASP.NET Web Forms. This article is the first of two articles migrating the <code>ProductDetail</code> page.</p> <p>In this article we will explore how to migrate ASP.NET WebForms ASCX controls into DotVVM. We will discuss the differences and challenges that need to be taken into account.</p> <p>You can follow this this migration by cloning <a href="https://github.com/riganti/dotvvm-samples-webforms-advanced">the sample repo</a>.</p> <h2 id="differences-between-aspnet-webforms-and-dotvvm-controls">Differences between ASP.NET WebForms and DotVVM controls</h2> <p>To understand how to successfully migrate ASCX controls we need to take a closer look at the structure of ASCX controls in contrast with DotVVM controls. Unlike Web Forms pages, where most of the markup and functionality has direct equivalent, DotVVM controls and ASCX controls have many differences. DotVVM controls are designed to operate on the client side using a combination of <a href="https://www.knockoutjs.com">Knockout JS</a> bindings and DotVVM JavaScript API.</p> <h3 id="web-forms-controls">Web Forms controls</h3> <p>ASPX controls usually consist of 3 files. One auto-generated designer file, one ASCX markup file, and one code-behind C# file. It is common to put the control logic in the C# code-behind and also to represent state in the viewstate of the control.</p> <p><img width="640" height="265" title="Structure of ASCX control" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="Structure of ASCX control" src="https://dotvvmstorage.blob.core.windows.net/blogpost/8f265e92-24aa-486d-a8cb-43aeaa6b4819.png" border="0"></p><p>The control properties are usually defined in C# as public properties on the code-behind class. The properties are evaluated on the server side only.</p> <p>The property values are stored as part of the viewstate field and are transferred during the post-back from client side to server side.</p> <h3 id="dotvvm-controls">DotVVM controls</h3> <p>DotVVM Markup controls are usually defined in <code>.dotcontrol</code> markup file. Optionally, the markup control can have a code-behind file with properties and additional rendering logic. In some cases we need to represent the control state, or the control has some validation logic. In such cases, we recommend creating a special viewmodel for the control. The viewmodel then holds all the data specific to the control and may contain any business logic.</p> <p><img width="640" height="229" title="Structure of DotVVM control" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="Structure of DotVVM control" src="https://dotvvmstorage.blob.core.windows.net/blogpost/ddcf1f79-2823-42e1-b011-dfd6de56e375.png" border="0"></p> <p>We refer to properties of DotVVM controls as DotProperties. DotProperties serve as proxies for the viewmodel data. The viewmodel data are bound to the DotProperties in the DotVVM page or another control from where the control is referenced. The properties must work both on the server side and on the client side.</p> <p>On the server side, the DotProperties are evaluated using the data from the viewmodel that have been initialized on the first page load, or transferred from the client during page the post-back.</p> <p>On the client side, the DotProperty value is represented as a <code>Knockout JS</code> computed observable which can be used in data-binding expressions. When the underlying data in the viewmodel property change, the DotProperty value also changes thanks to the Knockout JS notification system in the observables.</p> <h3 id="using-dotproperties">Using DotProperties</h3> <p>From DotVVM 4 onwards, it is possible to define DotProperty directly in the markup using <code>@property</code> directive. No code-behind class is needed.</p> <pre class="brush:dothtml"><code class="language-DOTHTML">@property bool UseHeader = true</code></pre> <p>There are some pitfalls to be aware of when using DotVVM controls and DotProperties:</p> <ul> <li>Simple C# public properties on markup control code-behind will not work as DotProperties and should be avoided.</li> <li>During the post-back, the DotProperty values are loaded in the <code>Load</code> stage. If you try to access them earlier (e. g. in the <code>Init</code> phase), the values will not be loaded yet.</li> <li>DotProperties are just proxies for the viewmodel and can not store their values. Therefore, DotProperties of primitive types cannot be assigned to.</li> <li>When DotProperties are bound to a complex object, we can set the properties of that object.</li> </ul> <h2 id="migrating-a-simple-control">Migrating a simple control</h2> <p>Now we have the theory out of the way, we can start migrating some controls.</p> <p>As a part of our <code>ProductDetail</code> page, we have a list of tags. The list is provided to the control as a property from the parent page. We can also optionally set the <code>AllowEditing</code> property and whether a link to the tag edit page is shown.</p> <p><img width="264" height="108" title="ASP.NET Web Forms control" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="ASP.NET Web Forms control" src="https://dotvvmstorage.blob.core.windows.net/blogpost/d521bd6c-7110-4168-9827-ca309618132d.png" border="0"></p> <p>The parent page has the responsibility of loading and filling the data. Parent page also calls the <code>DataBind</code> for the control.</p> <p>We can take a look at the code-behind <code>ProductTags.aspx.cs</code></p> <pre class="brush:csharp"><code class="language-CSHARP">public partial class ProductTags : System.Web.UI.UserControl { public List&lt;Tag&gt; Tags { get; set; } public bool AllowEditing { get; set; } public override void DataBind() { TagRepeater.DataSource = Tags; TagRepeater.DataBind(); if (AllowEditing) { AddTagPanel.Visible = true; } base.DataBind(); } //... }</code></pre> <p>The markup is just one Repeater control with the link that is shown based on the specified condition.</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;%@ Control Language="C#" AutoEventWireup="true" CodeBehind="ProductTags.ascx.cs" Inherits="DotVVM.Samples.Controls.ProductTags" %&gt; &lt;div class="product-tags"&gt; &lt;div id="EditTagPanel" runat="server" visible="false"&gt; You can &lt;a href="EditTags.aspx?productId=&lt;%=ProductId %&gt;"&gt;edit tags&lt;/a&gt;. &lt;/div&gt; &lt;asp:Repeater ID="TagRepeater" runat="server"&gt; &lt;ItemTemplate&gt; &lt;span class="product-tag"&gt;&lt;%# Eval("Name") %&gt;&lt;/span&gt; &lt;/ItemTemplate&gt; &lt;/asp:Repeater&gt; &lt;/div&gt;</code></pre> <p>With such a simple control, we do not need a viewmodel. We can rely on DotProperties that we can define in the <code>.dotcontrol</code> markup file.</p> <p>We can see in the <code>.aspx.cs</code> file are 2 properties that are set from parent page:</p> <pre class="brush:csharp"><code class="language-CSHARP">public List&lt;Tag&gt; Tags { get; set; } public bool AllowEditing { get; set; }</code></pre> <p>On top of that, we can see that the link uses the <code>ProductId</code> property to pass into the query string. With this information, we can create the new markup control <code>ProductTags.dotcontrol</code> and define its properties in the control markup:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">@import System.Collections.Generic @import DotVVM.Samples.Model @viewModel object @property List&lt;Tag&gt; Tags @property bool AllowEditing @property int ProductId</code></pre> <p>Notice that we used <code>@import</code> directive to import namespaces for <code>List</code> and <code>Tag</code>. Also we don't need any viewmodel for this control so we can use <code>object</code>. It declares that the control must be used in a data context that inherits from <code>System.Object</code> (which is true for any type in .NET).</p> <p>Now the properties can be set in the parent page <code>ProductDetail</code> like this:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;cc:ProductTags Tags={value: Tags} AllowEditing="true" ProductId={value: ProductId} /&gt;</code></pre> <p>We must not forget that DotVVM control properties are only proxies. They just contain whatever viewmodel data we bind to them. We need to create the <code>Tags</code> and <code>ProductId</code> properties in the viewmodel of our parent page to hold the data for us.</p> <p>We add the property to <code>ProductDetailViewModel.cs</code>:</p> <pre class="brush:csharp"><code class="language-CSHARP">public class ProductDetailViewModel : SiteViewModel{ //... public int ProductId { get; set; } public List&lt;Tag&gt; Tags { get; set; } //... } </code></pre> <p>We continue by migrating the <code>Repeater</code> control. First, let's remind ourselves of the <code>BindData</code> function of the original ASPX control code behind:</p> <pre class="brush:csharp"><code class="language-CSHARP">TagRepeater.DataSource = Tags; TagRepeater.DataBind();</code></pre> <p>Instead of setting the data source in the code, in DotVVM we use the <code>DataSource</code> binding in the markup to bind the <code>Tags</code> control property. No need to call any <code>DataBind</code> method - the data will be updated automatically. Since we are referencing DotProperty and not a viewmodel property, we have to add <code>_control</code> keyword. The <code>_control</code> keyword represents the markup control object on which our DotProperties are defined.</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;div class="product-tags"&gt; &lt;dot:Repeater ID="TagRepeater" DataSource={value: _control.Tags}&gt; &lt;span class="product-tag" InnerText={value: Name} /&gt; &lt;/dot:Repeater&gt; &lt;!-- ... --&gt; &lt;/div&gt;</code></pre> <p>Next, we can move to migrating the link. We can use DotVVM control <code>RouteLink</code>. The advantage of using the control instead of just <code>a</code> tag is we have validation of the route name and its parameters.</p> <p>Before migrating the link, we should create and register the <code>TagEdit</code> page just to be able to reference the route in the link.</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;div id="EditTagPanel" runat="server" visible="false"&gt; You can &lt;a href="EditTags.aspx?productId=&lt;%=ProductId %&gt;"&gt;edit tags&lt;/a&gt;. &lt;/div&gt;</code></pre> <p>The link references <code>EditTags.aspx</code> page so in DotVVM we are going to have the route registered as <code>EditTags</code>. Next we can see <code>ProductId</code> is used as a query parameter in the link. For query parameters we can use <code>Query-</code> property group of <code>RouteLink</code>.</p> <p>We should also address the conditional hiding of the edit link. In the <code>ProductTags.ascx</code> we can see following piece of code:</p> <pre class="brush:csharp"><code class="language-CSHARP">if (AllowEditing) { EditTagPanel.Visible = true; }</code></pre> <p>In the markup, we can see the <code>div</code> has attributes <code>runat="server" visible="false"</code> which tells us that it is hidden by default. For hiding parts of the page on server side that should not be rendered on client if some condition is not met we can use <code>IncludeInPage</code> attribute in combination with <code>resource</code> binding. This approach useful for conditions like privilege checks because the hidden element is not rendered on client side at all and cannot be shown using JavaScript. Of course, multiple layers of checks are advised.</p> <p>Putting it all to gether we get following migrated markup:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;div id="EditTagPanel" IncludeInPage={resource: _control.AllowEditing}&gt; You can &lt;dot:RouteLink RouteName="EditTags" Text="edit tags" Query-productId={value: _control.ProductId} /&gt;. &lt;/div&gt;</code></pre> <p>Now we have the <code>ProductTags</code> control migrated, We need to register it in the <code>DotvvmStartup.RegisterControls</code> method. This is so that we can reference it in the DotVVM markup.</p> <pre class="brush:csharp"><code class="language-CSHARP">config.Markup.AddMarkupControl("cc", "ProductTags", "Migrated/Controls/ProductTags.dotcontrol");</code></pre> <h2 id="loading-the-data">Loading the data</h2> <p>We have already referenced the <code>ProductTags</code> in the <code>ProductDetail</code> markup. We have also created the <code>ProductId</code> and <code>Tags</code> properties in <code>ProductDetailViewModel</code>. Now we have to migrate the logic of the product detail page that fills the properties. We can take a look at the <code>Page_Load</code> method in the <code>ProductDetail.aspx.cs</code> code-behind:</p> <pre class="brush:csharp"><code class="language-CSHARP">protected void Page_Load(object sender, EventArgs e) { var context = HttpContext.Current; _productId = context.GetIntQuery($"productId"); if (_productId == 0) { Message = "Invalid product ID"; return; } Tags = _facade.GetTags(_productId); //... BindData(); }</code></pre> <p>We can copy the content of the method into the <code>ProductDetailViewModel.Load()</code> method and start dealing with inconsistencies.</p> <p>For the logic filling the <code>_productId</code> from query the solution is quite simple. We already have our viewmodel property <code>ProductId</code>. Instead of <code>_productId</code> we use our viewmodel property <code>ProductId</code>.</p> <p>The ID of the current product is taken from the query string. DotVVM can automatically fill the value from query string into the viewmodel property. We have to use <code>[FromQuery("productId")]</code>. Just like that, the <code>ProductId</code> will be filled with the correct value. We do not need to get the value from the <code>HttpContext</code> ourselves.</p> <p>We have to keep the validation message. We create <code>Message</code> property as a new <code>string</code> property in the viewmodel. The <code>Message</code> has <code>private</code> setter because we intend to only send save messages from server to client, not from client back to server.</p> <p>We need to create a private readonly field <code>_facade</code> in <code>ProductDetailViewModel</code>. For this article, we will create the facade instance in the constructor like so:</p> <pre class="brush:csharp"><code class="language-CSHARP">public ProductDetail() { _facade = new ProductDetailFacade(); }</code></pre> <p>If the project used dependency injection, we would be able to inject the <code>_facade</code> using a constructor parameter.</p> <p>The last thing to take a look at, is the <code>BindData</code> method. In original ASPX page, the properties of the <code>ProductTagsControl</code> were set, and <code>ProductTagsControl.DataBind()</code> was called. We do not need any of that code; in DotVVM we are using data bindings in the page markup instead. There is also a code for data-binding another control, but we will deal with the control in the next article. We can safely delete the <code>DataBind</code> method call.</p> <p>The migrated <code>Load()</code> method looks like this:</p> <pre class="brush:csharp"><code class="language-CSHARP">public override Task Load() { if (ProductId == 0) { Message = "Invalid product ID"; return base.Load(); } Tags = _facade.GetTags(ProductId); return base.Load(); }</code></pre> <p>Now the <code>ProductTags</code> control should be migrated connected and functioning.</p> <h2 id="conclusion">Conclusion</h2> <p>In this article, we have explored some differences between ASP.NET Web Forms controls and DotVVM control. We explored the concept of DotProperties and contrasted them to Web Forms control properties. We migrated a simple <code>ProductTags</code> ASCX control into DotVVM control and we used markup-defined DotProperties. As a result, we ended up with less code and better maintainability.</p> <p>In the next article, we will explore migrating more complicated control that we may encounter in the wild.</p>Thu, 12 Oct 2023 16:04:48 ZTomáš Hercegtomas.herceg@riganti.czAnnouncing Preview 7 of DotVVM 4.2.0<p>We’ve just published a new <strong>public preview of DotVVM 4.2</strong> with a version tag <strong>4.2.0-preview07-final</strong>. This version is available for all packages: open-source DotVVM framework, <a href="https://github.com/riganti/dotvvm-contrib">DotVVM Contrib controls</a>, <a href="https://www.dotvvm.com/products/bootstrap-for-dotvvm">Bootstrap for DotVVM</a>, and <a href="https://www.dotvvm.com/products/dotvvm-business-pack">DotVVM Business Pack</a>. All these packages are compatible with each other and have been tested together.</p><p>This is most probably the last preview before releasing the stable version of DotVVM 4.2, so we will be happy for any feedback or bug reports so we can fix it in this version.</p><p>What features you can look forward to?</p><h2>Validation in static commands</h2><p>The <strong>AllowStaticCommand </strong>attribute now has a new parameter which can specify whether the validation of command arguments should happen automatically (this is the Automatic mode), or whether the static command may emit model state errors and send them back to the client by calling <strong>FailOnInvalidModelState</strong>.</p><pre class="brush:csharp"><code class="language-CSHARP">[AllowStaticCommand(StaticCommandValidation.Manual)] public string MyMethod(MyModel model) { var modelState = new StaticCommandModelState(); modelState.AddArgumentError(() =&gt; model.Property, "Property is invalid"); modelState.FailOnInvalidModelState(); }</code></pre><p>We believe that this feature will enable using static commands as a default way of calling the server. In general, static commands transfer less data and are faster as they don’t need to work with the entire viewmodel (serialization, deserialization, transferring it, comparing diffs, and so on). On the other hand, in case of static command validation, you need to perform a slight mental shift – you are not validating the viewmodel, but the arguments of the method. Thus, you are not able to emit a validation error for a property which you don’t pass to the static command as an argument.</p><p>See <a href="https://forum.dotvvm.com/t/4-2-preview-validation-in-staticcommands-and-more/43">related forum post</a> for more examples.</p><h2>Custom primitive types</h2><p>A lot of users like Domain-Driven Design or other architectural approaches where you typically work with strongly typed identifiers of domain objects. You don’t want to mess up an ID of an order with an ID of a customer, thus you have a special <strong>OrderId</strong> and <strong>CustomerId</strong> types. Often, both of them contain a <strong>Guid</strong>, but since they have different types, the compiler doesn’t allow you to assign one in the other.</p><p>DotVVM now allows to work with these types by implementing <strong>IDotvvmPrimitiveType</strong>. On the client side, they have to be represented as strings, so they need to implement the <strong>TryParse</strong> and <strong>ToString</strong> methods. </p><p>Thanks to that, you can use them as route or query string parameters, as a selected value in <strong>ComboBox </strong>and other controls (where it needs to be a primitive type), and you can compare them easily in expressions.</p><pre class="brush:csharp"><code class="language-CSHARP">public struct OrderId : IDotvvmPrimitiveType { public int Value { get; } public OrderId(int value) { this.Value = value; } public override string ToString() =&gt; Value.ToString(); public static bool TryParse(string value, out OrderId result) { if (int.TryParse(value, out var resultValue)) { result = new OrderId(resultValue); return true; } result = default; return false; } }</code></pre><h2>Metrics and Prometheus support</h2><p>We instrumented DotVVM with <a href="https://learn.microsoft.com/en-us/dotnet/core/diagnostics/metrics-instrumentation">System.Diagnostics.Metrics</a> so you can watch key performance metrics of your application, including sizes of the viewmodels, time the DotVVM needs for serialization and deserialization, and plenty of others.<p>If you want to use the <a href="https://github.com/prometheus-net/prometheus-net">prometheus-net</a> library to expose the metrics in <a href="https://prometheus.io/">prometheus</a> format, we recommend calling the <code>Prometheus.MeterAdapter.StartListening</code> method as soon as possible (at the start of Startup/DotvvmStartup is a good place), and configuring the buckets for histograms.<pre class="brush:csharp"><code class="language-CSHARP">MeterAdapter.StartListening(new MeterAdapterOptions { ResolveHistogramBuckets = instrument =&gt; { // prometheus-net does not know which buckets will make sense for each histogram and System.Diagnostics.Metrics API // does not provide a way to specify it. The ResolveHistogramBuckets function will be called for each exported histogram in to define the buckets. return DotvvmMetrics.TryGetRecommendedBuckets(instrument) ?? MeterAdapterOptions.DefaultHistogramBuckets; } });</code></pre><h2>WebForms adapters</h2><p>We have resurrected an old package which helps in the process of <a href="https://www.dotvvm.com/modernize">migrating Web Forms apps to the new .NET using DotVVM</a>. Because the process is done incrementally (page by page), for most of the time you have some pages written in DotVVM and some in Web Forms. You cannot use DotVVM RouteLink control to point to a page which hasn’t been migrated yet, and you don’t want to hard-code URLs to the pages because you’d lose all benefits of the routing system.<p>The <a href="https://www.nuget.org/packages/DotVVM.Adapters.WebForms">DotVVM.Adapters.WebForms</a> package contains the <code>HybridRouteLink</code> control which is basically a <strong>RouteLink </strong>which can also look in the Web Forms route table and generate links to not-yet-migrated pages, and there is also the <strong>RedirectToRouteHybrid</strong> method which does the same thing with redirects: if the route is present in DotVVM route table, it will use it, otherwise it falls back to the Web Forms route table.<h2>Bug fixes in Bootstrap 5 </h2><p>A lot of users started using the <strong>Bootstrap 5 </strong>version of <a href="https://www.dotvvm.com/products/bootstrap-for-dotvvm">Bootstrap for DotVVM</a>. This version is utilizing the new Composite controls approach, so we are still catching and fixing bugs – not only in the library itself, but we also want to improve the experience in the <a href="https://www.dotvvm.com/products/visual-studio-extensions">DotVVM for Visual Studio extension</a>.</p><p><br></p><p>If you try to upgrade your projects to this new preview versions, we’ll be happy to hear about your experience on <a href="https://forum.dotvvm.com"><strong>our new DotVVM Forum</strong></a><strong> </strong>(a replacement for Gitter). </p>Sat, 07 Oct 2023 13:41:24 ZTomáš Hercegtomas.herceg@riganti.czExtend static command capabilities with JavaScript translations<p><a href="https://www.dotvvm.com/docs/4.0/pages/concepts/respond-to-user-actions/static-commands">Static commands</a> are a powerful feature of DotVVM which lets you modify the viewmodel without calling the server. This is possible because DotVVM can translate some C# calls into JavaScript snippets. We don’t involve WebAssembly in this process – the translation is just a simple transform of the C# expression to a JS one, and the <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/data-binding/supported-expressions">list of supported syntax constructs and APIs is limited</a>. </p><p>Sometimes, the behavior of the translated expression is slightly different. The most visible nuance is that DotVVM bindings are propagating null values, so the C# expression <strong>Object.SomeProperty</strong> will be treated as <strong>Object?.SomeProperty</strong>. </p><h2>Providing custom translations</h2><p>You can register your own JavaScript translations to let DotVVM translate methods it doesn’t know about. You can use this mechanism to either allow calling your custom JS code from bindings, or to just fill the gaps in the methods DotVVM supports out of the box.</p><p>Imagine you want to create a <strong>GridView </strong>with an option to select the rows. Each row will have the <strong>IsSelected</strong> property to indicate whether the row is selected. The <strong>CheckBox </strong>in the header cell should be able to select or deselect all rows, and ideally without calling the server.</p><p>The data-bindings don’t support foreach loops, however the <strong>List&lt;T&gt; </strong>class has the <strong>ForEach </strong>method which accepts a lambda function. DotVVM cannot translate it out of the box, but you can extend the translation quite easily.</p><pre class="brush:dothtml"><code class="language-DOTHTML">&lt;dot:GridView DataSource="{value: Entries}"&gt; &lt;dot:GridViewTemplateColumn&gt; &lt;HeaderTemplate&gt; &lt;dot:CheckBox Checked="{value: Entries.All(e =&gt; e.IsSelected)}" Changed="{staticCommand: var newState = !Entries.All(e =&gt; e.IsSelected); Entries.ForEach(e =&gt; e.IsSelected = newState)}"/&gt; &lt;/HeaderTemplate&gt; &lt;ContentTemplate&gt; &lt;dot:CheckBox Checked="{value: IsSelected}" /&gt; &lt;/ContentTemplate&gt; &lt;/dot:GridViewTemplateColumn&gt; &lt;dot:GridViewTextColumn ValueBinding="{value: Name}" HeaderText="Item" /&gt; &lt;/dot:GridView&gt;</code></pre><p>You can see that the <strong>CheckBox </strong>in the table header first determines whether all rows are already selected. Then, it uses <strong>ForEach </strong>to set the <strong>IsSelected </strong>property to the new state.</p><h2>How the viewmodels looks like in JavaScript</h2><p>On the client, the viewmodel is represented as a hierarchy of Knockout observables. You can imagine the observable as a C# property represented by function which implements both get and set functionality. If you call the function without parameters, it is a getter. If you call it with one parameter, it is a setter.</p><pre class="brush:js"><code class="language-JS">const viewModel = { SomeProperty: ko.observable("initial value") }; // get const value = viewModel.SomeProperty(); // set viewModel.SomeProperty("new value");</code></pre><p>The viewmodel in JS will have the same structure as the viewmodel in C# (it is just JSON-serialized), but each expression is wrapped in these Knockout observables, so to read the value of <strong>Object.SomeProperty</strong> you’ll need to emit <strong>Object().SomeProperty()</strong>. DotVVM already contains an API to build these expression – you can find it in the <strong>DotVVM.Framework.Compilation.Javascript.Ast</strong>, so there is no need to build these pieces of JS code yourself. The expressions can be combined and nested, and the engine will add parentheses automatically based on the operator priority. This leads to a much cleaner output code.</p><h2>What we need to emit</h2><p>In our case, we need to translate the expression <strong>Entries.ForEach(e =&gt; e.IsSelected = newState)</strong> to a <strong>forEach </strong>method on a JS array – in Knockout syntax, it will be something like <strong>vm.Entries().forEach(e =&gt; e().IsSelected(newState))</strong>. Since DotVVM can already translate the inner lambda and the expression on which we call <strong>Entries</strong>, the only remaining bit is to provide the mapping of the <strong>List&lt;T&gt;.ForEach</strong> method to calling the <strong>forEach</strong> function.</p><p>In <strong>DotvvmStartup</strong>, we can add the following registration:</p><pre class="brush:csharp"><code class="language-CSHARP">config.Markup.JavascriptTranslator.MethodCollection.AddMethodTranslator( typeof(List&lt;&gt;).GetMethod("ForEach"), // method to translate new GenericMethodCompiler( // args[0] is this, the others are the arguments passed to the method args =&gt; args[0].Member("forEach").Invoke(args[1]) .WithAnnotation(ShouldBeObservableAnnotation.Instance) ));</code></pre><p>The first argument is the method we are translating. Since we don’t know the concrete type in the list and the translation would work for all list types, we can use open generic definition.</p><p>The second argument is <strong>GenericMethodCompiler</strong> which tells DotVVM how to translate the method call. Basically, it needs a lambda which accepts an array of <strong>JsExpression </strong>objects (first item is <strong>this</strong>, the other items are the arguments passed to the function), and it should bind these objects together.</p><p>In our case, <strong>args[0]</strong> will contain the <strong>JsExpression</strong> representing the Entries part of the expression – the object on which we are calling the method. It is a List&lt;T&gt; in C#, and it will be represented as an array in JavaScript.</p><p>We need to call the forEach function on this JS array, so we’ll call <strong>.Member(“forEach”).Invoke(…)</strong>. These methods are building an expression tree – the first one adds “<strong>.forEach</strong>”, the second one adds the parentheses and the arguments. The method has only one argument – the lambda which we should have in <strong>args[1]</strong> because it was the only argument passed to the C# <strong>ForEach</strong> method.</p><p>The last important thing is that we need to add <strong>ShouldBeObservableAnnotation</strong>. Annotations are a way to tell DotVVM what it can expect on various places in the expression tree. If we use this annotation, DotVVM will make sure that the arguments will be passed as Knockout observables (if possible), so the lambda could change them. If we don’t use the annotation, DotVVM would use <strong>dotvvm.state</strong> on the JS side (it is a snapshot of the viewmodel without the observables), so any change made to the objects would be lost. </p><p><br></p><p>If you try this yourself and look in the Dev Tools, you will see the emitted JavaScript:</p><p><img width="689" height="148" title="image" style="margin: 0px; border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="image" src="https://dotvvmstorage.blob.core.windows.net/blogpost/ecab2e69-c9e8-4ee3-9753-3d39404108ae.png" border="0"></p><p>You can see DotVVM added some extra things – there is a question mark in case the <strong>Entries </strong>would be null, it is also adding <strong>ko.unwrap(e) </strong>to the lambda parameter because it is not sure that the object in the variable is observable or not. Also, the lambda has an explicit return null because we didn’t provide any return value. </p><h2>Conclusion</h2><p>The JS translation engine is very powerful and can enhance the set of things that can be done on the client-side. It is not difficult to write a collection of C# methods (they even don’t need server-side implementation) and provide JS translations – this way you can call any browser API.</p><p>You can find <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/control-development/custom-javascript-translators">more info about JS translations in the documentation.</a></p>Sat, 12 Aug 2023 09:58:33 ZTomáš Hercegtomas.herceg@riganti.czMoving our Gitter chat to forum.dotvvm.com<p><strong>DotVVM Gitter </strong>was a popular place for our community. For years, it helped us to stay in touch with the community and respond their questions. </p><p>Recently, <a href="https://blog.gitter.im/2023/02/13/gitter-has-fully-migrated-to-matrix/">Gitter got a new interface as part of integration with Matrix</a>. While we appreciate the new user interface and features, it seems that we’ve lost one of the greatest advantages – <em>the searchability using Google</em>. Since there are not many tutorials and articles about DotVVM, except for the <a href="https://stackoverflow.com/questions/tagged/dotvvm">StackOverflow dotvvm tag</a> and the official documentation, <strong>being able to search for DotVVM-related questions and answers in Google is crucial for us</strong>.</p><p>What is more, although the Gitter interface supported threads, we weren’t always using them so the discussion got confusing when several topics got mixed up. </p><p><br></p><p>Therefore, <strong>we introduce a new place for the community – </strong><a href="https://forum.dotvvm.com"><strong>forum.dotvvm.com</strong></a>. To post questions, you need to create a separate account, or you can <em>sign in using GitHub</em>, same as on Gitter.</p><p><br></p><h2>Free and commercial support</h2><p>Currently, we are providing a free support on e-mail, StackOverflow, and the new forum. You can ask us anything and we’ll be happy to talk, however sometimes we are too busy to respond in a reasonable time.</p><p>That’s why <strong>we’ll be launching a commercial support program soon</strong>. This paid option will give you a guarantee of a response (end of the next business day), priority bug fixing, and more. We believe that this program will greatly improve the experience of DotVVM users who are working on large and mission-critical projects, and it will also help us with long-term development and support of the framework. Stay tuned!</p>Sat, 10 Jun 2023 15:23:43 ZMilan Mikušmilan.mikus@riganti.czApp Modernization - Part #2: Migrate underscore.js templates and JQuery data loading<p>This article is the second part of the series:</p> <ul> <li><a href="https://www.dotvvm.com/blog/104/App-Modernization-Part-1-Migrate-a-Web-Forms-app-to-DotVVM-and-NET-6">App Modernization - Part #1: Migrate Web Forms code islands to DotVVM</a></li> <li><strong><a href="https://www.dotvvm.com/blog/105/App-Modernization-Part-2-Migrate-underscore-js-templates-and-jquery-data-loading">App Modernization - Part #2: Migrate underscore.js templates and JQuery data loading</a></strong></li> <li><a href="https://www.dotvvm.com/blog/109/App-Modernization-Part-3-Migrate-ASCX-components">App Modernization - Part #3: Migrate ASCX components</a></li> <li><a href="https://www.dotvvm.com/blog/112/App-Modernization-Part-4-Migrate-ASCX-components">App Modernization - Part #4: Migrate ASCX components</a></li> </ul> <p>&nbsp;</p> <p>&nbsp;</p> <p>For this example, we will return to our fictional customer internal e-shop pages written in ASP.NET Web Forms. We will migrate an ASPX page that uses jquery and underscore to load and display data.</p> <h2 id="legacy-solution">Legacy solution</h2> <p>This example needs a bit of explanation beforehand. There is a page <code>Products.aspx</code> that displays a list of products. When the user clicks at the product row, a dialog with additional information opens. The data in the product table as well as data in the detail dialog are loaded using JQuery. The page which provides the data for the requests from <code>Products.aspx</code> is also an ASPX page <code>ProductsService.aspx</code>. The service page handles the request and displays JSON as the content of the page. The architecture of the legacy solution is as follows:</p> <pre><code>----------------- ------------------------ | Products.aspx | ------action:list---&gt; | ProductsService.aspx | ----------------- &lt;-------JSON--------- ------------------------ | | | ----action:detail---&gt; | | &lt;-------JSON--------- | </code></pre> <p>The files in our project structure we need to look at are these:</p> <pre><code>Model Product.cs Facades ProductFacade.cs Content products.js Pages Products.aspx Products.aspx.cs ProductsService.aspx ProductsService.aspx.cs </code></pre> <p><code>Products.aspx</code>:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;form id="ProductsForm" runat="server"&gt; &lt;script type="text/template" id="product-template"&gt; &lt;tr&gt; &lt;td&gt; &lt;input type="hidden" value="{{ product.Id }}"/&gt; {{ product.Code }} &lt;/td&gt; &lt;td&gt;{{ product.Name }}&lt;/td&gt; &lt;td&gt;{{ product.Price }} EUR&lt;/td&gt; &lt;/tr&gt; &lt;/script&gt; &lt;script type="text/template" id="product-dialog-template"&gt; &lt;div class="product-dialog-template"&gt; &lt;h3&gt;{{ product.Name }}&lt;/h3&gt; &lt;p&gt; &lt;span&gt;Code:&lt;/span&gt; {{ product.Code }} &lt;/p&gt; &lt;p&gt; &lt;span&gt;Price:&lt;/span&gt; {{ product.Price }} EUR &lt;/p&gt; &lt;p&gt; &lt;span&gt;Description:&lt;/span&gt; {{ product.Description }} &lt;/p&gt; &lt;/div&gt; &lt;/script&gt; &lt;div id="product-dialog"&gt;&lt;/div&gt; &lt;table id="products-table"&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Code&lt;/th&gt; &lt;th&gt;Name&lt;/th&gt; &lt;th&gt;Price&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;/form&gt;</code></pre> <p><code>products.js</code>:</p> <pre class="brush:js"><code class="language-JS">$(function () { _.templateSettings = { interpolate: /\{\{(.+?)\}\}/g }; const productTableTemplate = _.template($('#product-template').html()); const dialogTemplate = _.template($('#product-dialog-template').html()); const $productDialog = $('#product-dialog'); $productDialog.dialog({ autoOpen: false, modal: true, buttons: { "Close": function () { $(this).dialog("close"); } } }); // Sample product data $.get("/ProductsService.aspx", {}, function (products) { _.each(products, function (product) { $('#products-table tbody').append(productTableTemplate({ product: product })); }); }.bind(this)); // Show the product dialog on row click $('#products-table tbody').on('click', 'tr', function (sender, event) { const id = new Number($(sender.currentTarget).find("input[type=hidden]").attr("value")); $.get("/ProductsService.aspx", {action: "detail", id: id}, function (product) { $productDialog.html(dialogTemplate({ product: product })); $productDialog.dialog("open"); }.bind(this)); }); });</code></pre> <h2 id="fast-and-dirty">Fast and dirty</h2> <p>When we copy the content of the <code>Products.aspx</code> into a new DotVVM page, one issue is obvious. The DotVVM parses the template code islands as DotVVM bindings.</p> <p>One of the possible strategies is to make as few changes as possible. We could use <code>{{resource: }}</code> binding to &quot;escape&quot; the templates. This solution will allow you to migrate the page very fast. In most cases we found out that the javascript on the page works correctly after this change, and the page was usable. There are certainly downsides with this solution. The solution is bit fragile, there is no advantage in maintainability and adding new features is just as bothersome as it was before. But if this is just some unimportant page, hardly used, that is holding you from switching to latest .NET, this solution might work for you.</p> <h2 id="proper-migration">Proper migration</h2> <p>If you want a full migration to DotVVM, it will be bit more involved process, but there are also good news. We will gain strong typing and we will be able to completely get rid of javascript for pages like. The first course of action is to refactor templates into DotVVM controls and replace JQuery UI dialog with quite easy DotVVM equivalent.</p> <p>Plan of action is as follows:</p> <ol> <li>Create the <code>Products.dothtml</code> page without the templates</li> <li>Migrate the <code>product-template</code> into the DotVVM <code>Repeater</code> control</li> <li>Migrate the dialog into the <code>ProductDialog</code> markup control</li> <li>Migrate <code>ProductsService.aspx</code> to <code>ProductsUIService</code> for the on-demand loading of dialog content</li> </ol> <h3 id="1-the-page">Step 1: The page</h3> <p>Creating <code>Products.dothtml</code> page is quite straight-forward. We don't need any of the templates, we will deal with them separately. So we just need to copy over the table, and later we will add a custom control for the dialog.</p> <pre class="brush:dothtml"><code class="language-DOTHTML">@viewModel DotVVM.Samples.Migrated.Pages.Products.ProductsViewModel, TestSamples @masterPage Migrated/Pages/Site.dotmaster &lt;dot:Content ContentPlaceHolderID="MainContent"&gt; &lt;table id="products-table"&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Code&lt;/th&gt; &lt;th&gt;Name&lt;/th&gt; &lt;th&gt;Price&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;tbody&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;/dot:Content&gt;</code></pre> <p>In the the viewmodel, we prepare a collection to hold the products, and later we will add the property to accommodate the dialog.</p> <pre class="brush:csharp"><code class="language-CSHARP">public class ProductsViewModel : SiteViewModel { public List&lt;Product&gt; Products { get; set; } }</code></pre> <h3 id="2-the-product-table">Step 2: The product table</h3> <p>It is time to take care of <code>product-template</code> template. We could move the content of this template to a custom markup control. But since the table is simple, we may be able to get away with using it directly as a content of <code>&lt;dot:Repeater&gt;</code>.</p> <p>We look at the line <code>22</code> of the <code>products.js</code> JavaScript file;</p> <pre class="brush:js"><code class="language-JS">function (products) { _.each(products, function (product) { $(&#39;#products-table tbody&#39;).append(productTableTemplate({ product: product })); }); }.bind(this);</code></pre> <p>We can see <code>underscore.js</code> being used to iterate <code>products</code> using <code>each</code> and fill containing element with new elements created by evaluating <code>productTableTemplate</code> underscore.js template on data provided by the data provided by the elements of the <code>products</code> array.</p> <p>Such construction can be directly equated to DotVVM <code>&lt;dot:Repeater&gt;</code> control. The repeater iterates <code>Products</code> we provide as <code>DataSource</code> and uses provided <code>ItemTemplate</code> to fill the table. The data context for the <code>ItemTemplate</code> is the item of <code>Products</code> list.</p> <p>We can see that the code islands of the <code>product-template</code> template look a lot like bindings, and this is precisely what we will do with them. We will turn them into value bindings. Since data context of the <code>ItemTemplate</code> of a <code>&lt;dot:Repeater&gt;</code> is the item of the <code>Products</code> lists itself we don't need to specify <code>value: product.Something</code> for the bindings we can just write <code>value: Something</code>. Also we do not need the <code>&lt;input type=&quot;hidden&quot; ... /&gt;</code> because all the data are in the viewmodel.</p> <p>After migrating our <code>product-template</code> to DotHTML and using the <code>&lt;dot:Repeater&gt;</code> instead of the <code>_.each(...)</code> our table in <code>Products.dothtml</code> looks like this:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;table id=&quot;products-table&quot;&gt; &lt;thead&gt; &lt;tr&gt; &lt;th&gt;Code&lt;/th&gt;&lt;th&gt;Name&lt;/th&gt;&lt;th&gt;Price&lt;/th&gt; &lt;/tr&gt; &lt;/thead&gt; &lt;dot:Repeater DataSource={value: Products} WrapperTagName=&quot;tbody&quot;&gt; &lt;tr&gt; &lt;td&gt; {{ value: Code }} &lt;/td&gt; &lt;td&gt;{{ value: Name }}&lt;/td&gt; &lt;td&gt;{{ value: Price }} EUR&lt;/td&gt; &lt;/tr&gt; &lt;/dot:Repeater&gt; &lt;/table&gt;</code></pre> <p>Notice that we use the <code>WrapperTagName=&quot;tbody&quot;</code> property to tell the repeater to use <code>&lt;tbody&gt;</code> tag as a container for the rows.</p> <h3 id="3-the-dialog-template">Step 3: The dialog template</h3> <p>The original <code>Products.aspx</code> page uses a dialog to display detailed information about each product. We find the content of the dialog in underscore.js template with <code>id</code> of <code>product-dialog-template</code>. The template is evaluated in JavaScript and the dialog is opened as we can see on line <code>34</code> and <code>35</code> of <code>products.js</code> file.</p> <pre class="brush:js"><code class="language-JS">$productDialog.html(dialogTemplate({ product: product })); $productDialog.dialog(&quot;open&quot;);</code></pre> <p>Here, the raw HTML is being set into the <code>div</code> that will contain the dialog itself. JQuery library function <code>dialog</code> then builds the dialog and dialog overlay for us based on our content. Also note that the <code>dialogTemplate</code> template is initialized above in the javascript <code>projects.js</code> file:</p> <pre class="brush:js"><code class="language-JS">const dialogTemplate = _.template($(&#39;#product-dialog-template&#39;).html());</code></pre> <p>We need to represent this functionality in in DotVVM. First we need to migrate the content of the <code>product-dialog-template</code> template into DotVVM markup. The template code islands are quite straight forward we just replace them with value bindings.</p> <p>The migrated content of the <code>product-dialog-template</code> template:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;div class=&quot;product-dialog-template&quot;&gt; &lt;h3&gt;{{value: Name }}&lt;/h3&gt; &lt;p&gt; &lt;span&gt;Code:&lt;/span&gt; {{value: Code }} &lt;/p&gt; &lt;p&gt; &lt;span&gt;Price:&lt;/span&gt; {{value: Price }} EUR &lt;/p&gt; &lt;p&gt; &lt;span&gt;Description:&lt;/span&gt; {{value: Description }} &lt;/p&gt; &lt;/div&gt; &lt;dot:Button Click={staticCommand: IsVisible = false} Text=&quot;Close&quot; /&gt;</code></pre> <p>For now, we just migrate the content we will deal with using it in the page later.</p> <p>We added the &quot;Close&quot; button that we have seen in the dialog configuration on line <code>13</code> of the <code>products.js</code> file:</p> <pre class="brush:js"><code class="language-JS">buttons: { &quot;Close&quot;: function () { $(this).dialog(&quot;close&quot;); } }</code></pre> <p>It's job is semantically the same. It just closes the dialog. The tricky part is that we need to look in the ASPX page for the template and look in the JavaScript for the dialog configuration.</p> <p>Since the dialog has custom data to manage, best practice is to create <code>ProductDialogViewModel.cs</code> viewmodel for the dialog. The markup can guide us here. We start with empty viewmodel for the control and create the properties based on the bindings in the markup (<code>CTRL+.</code> and <code>Create property in the viewmodel</code> if you have pro version of the DotVVM for VisualStudio). Plus, for showing and hiding the dialog we use <code>IsVisible</code> property that we add to the dialog viewmodel.</p> <p><code>ProductDialogViewModel.cs</code></p> <pre class="brush:csharp"><code class="language-CSHARP">public class ProductDialogViewModel { public string Name { get; set; } public string Code { get; set; } public decimal Price { get; set; } public string Description { get; set; } public bool IsVisible { get; set; } }</code></pre> <p>With the dialog viewmodel ready, we can put a property for the dialog data into our main viewmodel <code>ProductsViewModel.cs</code>.</p> <pre class="brush:csharp"><code class="language-CSHARP">public ProductDialogViewModel Dialog { get; set; } = new ProductDialogViewModel();</code></pre> <p>Next we need to add the functionality of a dialog. We have several options, and we will explore all of them.</p> <ul> <li>DotVVM Bootstrap <code>ModalDialog</code> control</li> <li>DotVVM Business Pack <code>ModalDialog</code> control</li> <li>Vanilla DotVVM custom dialog</li> </ul> <h3 id="31-dialog-in-bootstrap-for-dotvvm">Step 3.1: Dialog in Bootstrap for DotVVM</h3> <p>If you have a license for <a href="https://www.dotvvm.com/products/bootstrap-for-dotvvm">Bootstrap for DotVVM</a>, you can use the <code>bs:ModalDialog</code> control. We would simply put the migrated <code>&lt;div class=&quot;product-dialog-template&quot;&gt;...&lt;div/&gt;</code> inside of the dialog <code>ContentTemplate</code>. The buttons have a separate template so we put <code>&lt;dot:Button ... Text=&quot;Close&quot; ... /&gt;</code> into the <code>FooterTemplate</code>. We would wire the DotVVM properties <code>DataContext</code> to the viewmodel property <code>Dialog</code>, and then we would use <code>IsDisplayed={value: IsVisible}</code> to control whether the dialog is shown or hidden.</p> <p>The page <code>Products.dothtml</code> with <code>bs:ModalDialog</code> included should then look like this:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;dot:Content ContentPlaceHolderID=&quot;MainContent&quot;&gt; &lt;bs:ModalDialog DataContext={value: Dialog} IsDisplayed=&quot;{value: IsVisible}&quot;&gt; &lt;div class=&quot;product-dialog-template&quot;&gt; &lt;h3&gt;{{value: Name }}&lt;/h3&gt; &lt;!-- ... The rest of the migrated content of the template from before --&gt; &lt;/div&gt; &lt;dot:Button Click={staticCommand: IsVisible = false} Text=&quot;Close&quot; /&gt; &lt;/bs:ModalDialog&gt; &lt;table id=&quot;products-table&quot;&gt; ... &lt;/table&gt; &lt;/dot:Content&gt;</code></pre> <p>The <strong>Bootstrap for DotVVM</strong> provides all the necessary functionality of the dialog making our job a bit easier.</p> <h3 id="32-dialog-in-dotvvm-dotvvm-business-pack">Step 3.2: Dialog in DotVVM Business Pack</h3> <p>With <a href="https://www.dotvvm.com/products/dotvvm-business-pack">DotVVM Business Pack</a>, it is a very similar story. We would put the migrated content of <code>&lt;div class=&quot;product-dialog-template&quot;&gt;...&lt;div/&gt;</code> together with <code>&lt;dot:Button ... Text=&quot;Close&quot; ... /&gt;</code> into the content of <code>bp:ModalDialog</code>. We would wire DotVVM properties <code>DataContext</code> to viewmodel property <code>Dialog</code>, and then we would use <code>IsDisplayed={value: IsVisible}</code>. This would save us from having a custom dialog.</p> <p>We can put the <code>bp:ModalDialog</code> control into the <code>Products.dothtml</code> page and fill the <code>HeaderTemplate</code>, <code>ContentTemplate</code>, <code>FooterTemplate</code>:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;dot:Content ContentPlaceHolderID=&quot;MainContent&quot;&gt; &lt;bs:ModalDialog DataContext={value: Dialog} IsDisplayed=&quot;{value: IsVisible}&quot;&gt; &lt;HeaderTemplate&gt; Product detail: {{value: Name}} &lt;/HeaderTemplate&gt; &lt;ContentTemplate&gt; &lt;div class=&quot;product-dialog-template&quot;&gt; &lt;h3&gt;{{value: Name }}&lt;/h3&gt; &lt;!-- ... The rest of migrated content --&gt; &lt;!-- from the template from before --&gt; &lt;/div&gt; &lt;/ContentTemplate&gt; &lt;FooterTemplate&gt; &lt;dot:Button Click={staticCommand: IsVisible = false} Text=&quot;Close&quot; /&gt; &lt;/FooterTemplate&gt; &lt;/bs:ModalDialog&gt; &lt;table id=&quot;products-table&quot;&gt; ... &lt;/table&gt; &lt;/dot:Content&gt;</code></pre> <p>The dialog migration is done, <strong>DotVVM Business Pack</strong> provides the necessary dialog functionality.</p> <h3 id="33-dialog-in-vanilla-dotvvm">Step 3.3: Dialog in vanilla DotVVM</h3> <p>Making dialog in vanilla DotVVM is not complicated at all. It just requires a few tricks we can explore.</p> <p>To keep our markup nice and tidy, we create new <code>ProductDialog.dotcontrol</code> control file with our already created <code>ProductDialogViewModel.cs</code> viewmodel. So we start the file with <code>@viewmodel</code> directive and copy the migrated content of the <code>product-dialog-template</code> template into the file. With this we should get a nice start for our <code>ProductDialog</code> markup control.</p> <p>We must not forget to register the markup control in the <code>DotvvmStartup</code>:</p> <pre class="brush:csharp"><code class="language-CSHARP">config.Markup.AddMarkupControl( &quot;cc&quot;, &quot;ProductDialog&quot;, &quot;Migrated/Pages/Products/Controls/ProductDialog.dotcontrol&quot;);</code></pre> <p>After we have prepared the basic migrated content for <code>ProductDialog</code>, we need to add the functionality of a dialog.</p> <p>All that said, creating dialog in vanilla DotVVM is not hard at all. We modify <code>ProjectDialog.dotcontrol</code> by wrapping the content in <code>&lt;div class=&quot;dialog&quot;&gt;&lt;/div&gt;</code> to serve as dialog body. Then we add <code>&lt;div class=&quot;overlay&quot; /&gt;</code> to serve as an modal dialog overlay.</p> <p>Of course we need to define the styles for the dialog to function correctly on the page. For that we need to define our css classes <code>overlay</code> and <code>dialog</code>. You can do it in the same control file, but since the CSS ca be reused for all dialogs usually we but it in the separate css file.</p> <pre class="brush:css"><code class="language-CSS">.dialog-overlay { z-index: 100; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: #aaaaaa; opacity: .3; }</code></pre> <p>High <code>z-index</code> will make sure our dialog and overlay is displayed well above content on the page <code>position: fixed; top: 0; left: 0; width: 100%; height: 100%;</code> will make our overlay cover the whole page corner to corner.</p> <pre class="brush:css"><code class="language-CSS">.dialog { z-index: 101; position: absolute; top: 35%; left: 35%; height: auto; width: 30%; border: 1px solid #999; border-radius: 4px; background-color: #fff; padding: 10px; }</code></pre> <p>The <code>z-index</code> needs to be above the value in <code>.dialog-overlay</code>. <code>position: absolute; top: 35% left: 35%; width: 30%;</code> is here to display the dialog centered on the page.</p> <p>The <code>ProjectDialog.dotcontrol</code> control should at this point be a viable dialog, now we need to add it to the <code>Projects.dothtml</code> page:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;dot:Content ContentPlaceHolderID=&quot;MainContent&quot;&gt; &lt;cc:ProductDialog DataContext={value: Dialog} /&gt; &lt;table id=&quot;products-table&quot;&gt; ... &lt;/table&gt; &lt;/dot:Content&gt;</code></pre> <p>For the dialog data we add <code>Dialog</code> property in the <code>ProductsViewModel</code>:</p> <pre class="brush:csharp"><code class="language-CSHARP">public ProductDialogViewModel Dialog { get; set; } = new ProductDialogViewModel();</code></pre> <h3 id="4-loading-the-data">Step 4: Loading the data</h3> <p>Now, when we have the UI ready, we can migrate the data loading. We have to load the data for the page itself, and we need to load the data for the dialog.</p> <p>Let's look at <code>products.js</code>. On line <code>21</code>, we make a <code>GET</code> request to <code>ProductsService.aspx</code> with no parameters to load JSON with list of projects;</p> <pre class="brush:js"><code class="language-JS">$.get(&quot;/ProductsService.aspx&quot;, {}, function (products) { ... });</code></pre> <p>On line <code>32</code>, we can see another request this time with parameters <code>detail</code> and <code>id</code>.</p> <pre class="brush:js"><code class="language-JS">$.get(&quot;/ProductsService.aspx&quot;, {action: &quot;detail&quot;, id: id}, function (product) { ... });</code></pre> <p>We need to look at <code>ProductsService.aspx</code> and see the ASPX page code behind implementation. (ASPX markup just displays <code>Json</code> property as literal.)</p> <pre class="brush:csharp"><code class="language-CSHARP">public partial class ProductsService : Page { private readonly ProductFacade facade; public ProductsService() { facade = new ProductFacade(); } public string Json { get; set; } protected void Page_Load(object sender, EventArgs e) { var context = HttpContext.Current; var action = context.GetQuery(&quot;action&quot;); var id = context.GetIntQuery(&quot;id&quot;); if (action == &quot;detail&quot; &amp;&amp; id &gt; 0) { Json = JsonConvert.SerializeObject(facade.Get(id) ?? new object()); } else { Json = JsonConvert.SerializeObject(facade.List()); } } }</code></pre> <p>This time we are lucky and both the detail operation and the list operations are nicely separated in the <code>ProductFacade</code>. In many projects where old ASPX pages are used as API to get JSON data, the actions could be one tangled mess. In that case, we would have to refactor the page and create our facade and refactor the code-behind close to something we can see here. That however would be whole another can of worms we are not going to open here.</p> <p>Now we know how <code>ProductsService.aspx</code> works, and we can take <code>ProductFacade</code> and reference it in our <code>ProductsViewModel</code>. We can take the <code>facade.List()</code> that is used for the <code>GET</code> request with no parameters and use it to load the products in <code>PreRender</code> function of the <code>ProductsViewModel</code> viewmodel. There is no point in loading the project in JavaScript. We can load them in C# and have type safety and better error checking.</p> <pre class="brush:csharp"><code class="language-CSHARP">public ProductsViewModel() { facade = new ProductFacade(); } public override Task PreRender() { if(!Context.IsPostBack) { Products = facade.List().ToList(); } return base.PreRender(); }</code></pre> <p>Notice that we only load the products on the first load of the page because DotVVM will take care of refilling the <code>Products</code> property on postbacks.</p> <p>To load the dialog data on demand we can use DotVVM UI service in combination with static command.</p> <p>The job of <code>ProductsService.aspx</code> will be taken by newly created <code>ProductsUiService</code>. The DotVVM UI Services can be referenced from the DotVVM pages by using <code>@service</code> directive. We can call a method of the UI service from static command binding and assign the result to <code>Dialog</code> property. This solution is ideal to load data od demand without needing to do a full postback.</p> <pre class="brush:csharp"><code class="language-CSHARP">public class ProjectsUiService { private readonly ProductFacade facade; public ProjectsUiService() { facade = new ProductFacade(); } [AllowStaticCommand] public ProductDialogViewModel GetDialog(int id) { var project = facade.Get(id); return new ProductDialogViewModel { Code = project.Code, Description = project.Description, IsVisible = true, Name = project.Name, Price = project.Price }; } }</code></pre> <p>The UI service is just simply uses the <code>ProductFacade</code> from <code>ProductsService.aspx</code> uses <code>Get(...)</code> to get the project and creates the dialog viewmodel. Notice that methods of the UI service intended to be called from static commands need to have <code>[AllowStaticCommand]</code> attribute.</p> <p>To be able to reference the UI service in the DotHTML markup we need to register it in the <code>DotvvmStartup</code>.</p> <pre class="brush:csharp"><code class="language-CSHARP">public void ConfigureServices(IDotvvmServiceCollection services) { ... services.Services.AddTransient&lt;ProjectsUiService&gt;(); }</code></pre> <p>Finally, we can reference the service in the <code>Projects.dothtml</code>:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">@service productsService = DotVVM.Samples.Migrated.Pages.Products.ProductsUiService</code></pre> <p>On line <code>80</code> of <code>products.js</code>, we see a JQuery event registration:</p> <pre class="brush:js"><code class="language-JS">$(&#39;#products-table tbody&#39;).on(&#39;click&#39;, &#39;tr&#39;, function (sender, event) { ... });</code></pre> <p>We can do the same job as JQuery <code>.click()</code> directly in our <code>Products.dothtml</code> markup file. We can use DotVVM static command binding. Having the <code>Click</code> binding directly in the markup is more intuitive for anyone reading the code. The JQuery <code>.click(...)</code> references a <code>tr</code> element inside of <code>products-table tbody</code>. Base on this we locate equivalent element in our <code>Products.dothtml</code> page, and add the binding like so:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;tr Events.Click={staticCommand: _root.Dialog = productsService.GetDialog(Id)}&gt;</code></pre> <p>The static command binding will call <code>GetDialog</code> of the UI service on the server side (without doing full post back) and sets property <code>Dialog</code> in the root viewmodel on the client side.</p> <h2 id="conclusion">Conclusion</h2> <p>By wiring in the click event of the table row the migration of our test page is complete. For our migrated DotVVM sample we crated new files:</p> <pre><code>Migrated Pages Controls ProductDialog.dotcontrol ProductDialogViewModel.cs Products.dothtml ProductsViewModel.cs ProductsUiService.cs </code></pre> <p>On top of that we reused <code>ProductsFacade.cs</code>. We don't need <code>projects.js</code> as we migrated all the JavaScript logic into C# and DotHTML. We also don't need <code>ProjectsService.aspx</code>. We replaced its functionality with <code>ProductDialogViewModel</code> and <code>ProductsUiService</code>.</p> <p>This migration was a bit more complicated in a sense that it is not possible to just mechanically do text replaces. The principles and patterns used here however can be used generally.</p> <p>In particular, the way to migrate <code>underscore.js</code> templates and then turn them into either DotVVM markup controls, or put them as a content of DotVVM template property of a control like <code>&lt;dot:Repeater&gt;</code>.</p> <p>Another useful pattern is that we can replace on-demand data loading in JavaScript like JQuery <code>.get(...)</code> by using DotVVM UI Services in tandem with static command bindings to load the data on demand without full post back.</p>Wed, 24 May 2023 08:00:00 ZMilan Mikušmilan.mikus@riganti.czApp Modernization - Part #1: Migrate Web Forms code islands to DotVVM and .NET 6<p>This article is the first part of the series:</p> <ul> <li><strong><a href="https://www.dotvvm.com/blog/104/App-Modernization-Part-1-Migrate-a-Web-Forms-app-to-DotVVM-and-NET-6">App Modernization - Part #1: Migrate Web Forms code islands to DotVVM</a></strong></li> <li><a href="https://www.dotvvm.com/blog/105/App-Modernization-Part-2-Migrate-underscore-js-templates-and-jquery-data-loading">App Modernization - Part #2: Migrate underscore.js templates and JQuery data loading</a></li> <li><a href="https://www.dotvvm.com/blog/109/App-Modernization-Part-3-Migrate-ASCX-components">App Modernization - Part #3: Migrate ASCX components</a></li> <li><a href="https://www.dotvvm.com/blog/112/App-Modernization-Part-4-Migrate-ASCX-components">App Modernization - Part #4: Migrate ASCX components</a></li> </ul> <p>&nbsp;</p> <p>&nbsp;</p> <p>In recent weeks, <a href="https://www.riganti.cz/en"><strong>RIGANTI</strong></a> has finished a project where we had to <strong>migrate a legacy ASP.NET Web Forms </strong>app to <strong>.NET 6</strong>. The application was written in C# and .NET Framework, it was using JQuery and Underscore libraries, and there was a big bunch of the business logic without detailed documentation. The reason for the migration was to be able to <strong>run the application on Linux </strong>and <strong>preserve most of the functionality</strong>. Full rewrite of the application would be too costly as the application was mostly an internal tool. The users preferred <strong>keeping the appearance and behavior of the application as close to the original </strong>one as possible.</p> <p>In this series of articles, I’d like to share some insights and common situations you’ll run into when you use <a href="https://www.dotvvm.com/modernize">DotVVM for the modernization of legacy apps</a>.</p> <h2>Migration of <code>if</code>, <code>else if</code>, <code>for</code> and <code>foreach</code> code islands</h2> <p>Let's imagine that we have an old e-commerce with a page for gathering customer feedback of a product:</p> <p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;div id="ResultsDiv" runat="server"&gt; &lt;table id="notes"&gt; &lt;% if (!string.IsNullOrEmpty(Error)) { %&gt;&lt;tr&gt;&lt;td colspan="3" class="error"&gt;&lt;%=Error %&gt;&lt;/td&gt;&lt;/tr&gt;&lt;% } %&gt; &lt;% if(User.IsInRole("Manager")) { %&gt; &lt;tr&gt; &lt;td colspan="3"&gt; &lt;% if(ProductId &gt; 0) { %&gt; &lt;a href="Products.aspx?productId=&lt;%=ProductId%&gt;"&gt;To product&lt;/a&gt; &lt;% } else if (CategoryId &gt; 0) { %&gt; &lt;a href="Categories.aspx?categoryId=&lt;%=CategoryId%&gt;"&gt;To category&lt;/a&gt; &lt;% } else { %&gt; &lt;a href="Dashboard.aspx"&gt;To Dashboard&lt;/a&gt; &lt;% } %&gt; &lt;/td&gt; &lt;/tr&gt; &lt;% } %&gt; &lt;tr&gt; &lt;th&gt;Date&lt;/th&gt; &lt;th&gt;User&lt;/th&gt; &lt;th&gt;Comment&lt;/th&gt; &lt;/tr&gt; &lt;% if( Notes == null || Notes.Count == 0 ) { %&gt; &lt;tr&gt; &lt;td colspan="3"&gt;No notes entered &lt;/td&gt; &lt;/tr&gt; &lt;% } else { %&gt; &lt;% foreach( var note in Notes ) { %&gt; &lt;tr&gt; &lt;td&gt;&lt;%=note.CreateDate.ToString( "MM/dd/yyyy hh:mm" )%&gt;&lt;/td&gt; &lt;td&gt;&lt;%=note.UserName%&gt;&lt;/td&gt; &lt;td&gt;&lt;%=note.Text%&gt;&lt;/td&gt; &lt;/tr&gt; &lt;% } %&gt; &lt;% } %&gt; &lt;/table&gt; &lt;/div&gt;</code></pre> <p>We have chosen DotVVM to migrate the e-shop because that way, we can use the old pages and the migrated pages side by side in the same web application, and after every page is migrated, we can switch the project to .NET 6.</p> <h3>Challenges</h3> <p>As we copy the content of the ASPX page into a new DotVVM page, we can see that the markup is just broken, and it would not be better the other way around. This is because the ASPX tooling cannot parse DotVVM bindings and likewise, DotVVM tooling does not understand ASPX code islands. </p> <p><a href="https://dotvvmstorage.blob.core.windows.net/blogpost/9fb70d83-e96d-4a99-becd-fe7041456fdd.png"><img width="670" height="246" title="The ASPX code block are not recognized in DotVVM page" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="The ASPX code block are not recognized in DotVVM page" src="https://dotvvmstorage.blob.core.windows.net/blogpost/e14e14b9-5f22-40ac-9bd2-14a800d651da.png" border="0"></a></p> <p>To convert the page markup in the DotVVM-friendly form we need to:</p> <ul> <li>get rid of the code islands,</li> <li>keep the hierarchy of the code blocks intact and represented is some kind of DotVVM markup structure. </li> </ul> <p>On top of that, we want to be able to use batch replace and regular expressions to save as much manual work as possible. It would be a bad day if we had to manually recreate every section where content of the page is conditionally displayed or hidden.</p> <h3>Strategy</h3> <p>The <code>if</code>, <code>else if</code>, and <code>else</code> code blocks are used to display or hide content of the page, in case of <code>for</code>/<code>foreach</code> to display the same content multiple times.</p> <p>One of our concerns is to make edits that keep the hierarchy of the code blocks. The second concern is that we can do a batch replace to save as much work as possible. The obstacle here are the closing brackets; <code>&lt;%}%&gt;</code>. They are not specific and can belong to any of the code blocks. Yet they are essential to keep the structure of the page intact. We need to choose a DotVVM control that would allow us to replace all the <code>&lt;%}%&gt;</code> with a closing tag. We also need some flexibility to replace different kinds of code blocks with opening tag.</p> <p>DotVVM has the <code>PlaceHolder</code> control which does not render anything. If some of its properties are set, it will render only a Knockout JS virtual element in the resulting HTML. We can use the control's <code>IncludeInPage</code> property to show or hide the content based on some expression coming from the viewmodel. </p> <p>We cannot tell whether the ASPX <code>&lt;%}%&gt;</code> is part of <code>if</code>, <code>else</code> or <code>for</code> block. Thus, we replace all code islands with <code>PlaceHolder</code> and then refactor <code>for</code>/<code>foreach</code> into <code>Repeater</code> controls later. The process goes like this:</p> <p>- We replace all <code>&lt;%}%&gt;</code> to <code>&lt;/dot:PlaceHolder&gt;</code><br> - We replace <code>&lt;% if ( /*condition here */ ) { %&gt;</code> to <code>&lt;dot:PlaceHolder IncludeInPage={value: condition here}&gt;</code>. To capture the condition we can use regular expressions.<br> - We replace <code>&lt;% } else { %&gt;</code> to <code>&lt;/dot:PlaceHolder&gt;&lt;dot:PlaceHolder IncludeInPage={value: "TODO: negated condition(s) here"}&gt;</code>. Else block require finishing by hand.<br> - We replace <code>&lt;% } else if ( /*condition here */ ) { %&gt;</code> to <code>&lt;/dot:PlaceHolder&gt;&lt;dot:PlaceHolder IncludeInPage={value: condition here &amp;&amp; "TODO: negated condition from previous if(s)"}&gt;</code><br> - Lastly, we need to deal with for/foreach blocks. To keep the replacements compatible, we also replace those with something like <code>&lt;dot:PlaceHolder DataSource={value: whatever we managed to capture from the for/foreach block}&gt;</code>. This block is usually the most non-standard so we need to creatively craft the regular expression. On top of that, sometimes the index is used to generate row number in a table or row coloring. This needs to be commented out and dealt with by hand.</p> <p>Few replaces later, we should have a DotVVM page looking like this:</p> <pre class="brush:dothtml"><code class="language-DOTHTML">&lt;div id="ResultsDiv" runat="server"&gt; &lt;table id="notes"&gt; &lt;dot:PlaceHolder IncludeInPage={value: !string.IsNullOrEmpty(Error) }&gt; &lt;tr&gt;&lt;td colspan="3" class="error"&gt;&lt;%=Error %&gt;&lt;/td&gt;&lt;/tr&gt; &lt;/dot:PlaceHolder&gt; &lt;dot:PlaceHolder IncludeInPage={value: User.IsInRole("Manager")}&gt; &lt;tr&gt; &lt;td colspan="3"&gt; &lt;dot:PlaceHolder IncludeInPage={value: ProductId &gt; 0}&gt; &lt;a href="Products.aspx?productId=&lt;%=ProductId%&gt;"&gt;To product&lt;/a&gt; &lt;/dot:PlaceHolder&gt; &lt;dot:PlaceHolder IncludeInPage={value: CategoryId &gt; 0 &amp;&amp; "TODO: negated condition from previous if(s)"}&gt; &lt;a href="Categories.aspx?categoryId=&lt;%=CategoryId%&gt;"&gt;To category&lt;/a&gt; &lt;/dot:PlaceHolder&gt; &lt;dot:PlaceHolder IncludeInPage={value: "TODO: negated condition(s) here"}&gt; &lt;a href="Dashboard.aspx"&gt;To Dashboard&lt;/a&gt; &lt;/dot:PlaceHolder&gt; &lt;/td&gt; &lt;/tr&gt; &lt;/dot:PlaceHolder&gt; &lt;tr&gt; &lt;th&gt;Date&lt;/th&gt; &lt;th&gt;User&lt;/th&gt; &lt;th&gt;Comment&lt;/th&gt; &lt;/tr&gt; &lt;dot:PlaceHolder IncludeInPage={value: Notes == null || Notes.Count == 0 }&gt; &lt;tr&gt; &lt;td colspan="3"&gt; No notes entered &lt;/td&gt; &lt;/tr&gt; &lt;/dot:PlaceHolder&gt; &lt;dot:PlaceHolder IncludeInPage={value: "TODO: negated condition from previous if(s)"}&gt; &lt;dot:PlaceHolder DataSource={value: var note in Notes }&gt; &lt;tr&gt; &lt;td&gt;&lt;%=note.CreateDate.ToString( "MM/dd/yyyy hh:mm" )%&gt;&lt;/td&gt; &lt;td&gt;&lt;%=note.UserName%&gt;&lt;/td&gt; &lt;td&gt;&lt;%=note.Text%&gt;&lt;/td&gt; &lt;/tr&gt; &lt;/dot:PlaceHolder&gt; &lt;/dot:PlaceHolder&gt; &lt;/table&gt; &lt;/div&gt;</code></pre> <p>The resulting markup is still far from being migrated, but now formatting and refactoring of the page has just became much more convenient. With that, let's correct the obvious issue caused by the replace:</p> <ul> <li>Line 6 can be simplified. We have a special control for this task called <strong>dot:RoleView </strong>we replace <code>PlaceHolder</code> to <code>RoleView</code> and put <strong>Manager</strong> in the <code>Roles</code> property</li> <li> On lines 9, 12 and 15, we have some TODOs to deal with. Unfortunately, we need to do this manually. This is just a logic exercise: we have to go top to bottom adding and negating the conditions as required.</li> <li>On line 34, the former foreach can now became a <code>dot:Repeater</code>. We know that because we left the original content of the foreach in the <code>DataSource</code> property. <code>DataSource</code> does not exist on a <code>PlaceHolder</code>. But we can change <code>PlaceHolder</code> to <code>Repeater </code> and the tag hierarchy will be preserved. We can change the binding <code>value: var note in Notes</code> to just <code>value: Notes</code> because we can see it will just bind on a collection in the viewmodel. Finally, we need to add <code>RenderWrapperTag="false"</code> to the repeater(s) because <code>dot:Repeater</code> renders a <code>div</code> element by default. The <code>div</code> would break our table.</li> <li>On line 33 the <code>PlaceHolder&nbsp;</code> can be removed completely as it's condition is integrated in the <code>Repeater</code> control by default.</li> <li>Next, we replace <code>&lt;%=SomeProperty %&gt;</code> with <code>&lt;dot:Literal Text={value: SomeProperty}&gt;</code>. There are few side notes to this. In the <code>href=""</code> attribute, we need to use interpolated string in the binding, because we cannot bind just half of the attribute. Inside the <code>Repeater</code>, we don't use <code>value: note.Text</code> in the bindings, but we simply use <code>value: Text</code> because the <code>Repeater</code> changed the data context for us. We need to craft a batch replace for such cases.</li> <li>We added <code>&lt;tbody&gt;</code> in our table because browsers may try to add it in and break our virtual elements in the resulting page.</li> <li>Finally, we can get rid of the <code>runat="server"</code>. Make sure the elements marked with <code>runat="server"</code> are not being referenced directly form the code-behind. Dealing with code-behind is beyond the scope of this article. </li> </ul> <p>After everything is done, we should now have a valid DotVVM markup. To make it work, I drafted out a viewmodel with properties. The resulting markup will not win any code beauty contests, but our goal here was to create a stable strategy for migrating 2000 lines of ASPX code quickly.</p> <p> <pre class="brush:dothtml"><code class="language-DOTHTML"> &lt;div id="ResultsDiv" runat="server"&gt; &lt;table id="notes"&gt; &lt;tbody&gt; &lt;dot:PlaceHolder IncludeInPage={value: !string.IsNullOrEmpty( Error ) }&gt; &lt;tr&gt;&lt;td colspan="3" class="error"&gt; &lt;dot:Literal Text={value: Error } /&gt;&lt;/td&gt;&lt;/tr&gt; &lt;/dot:PlaceHolder&gt; &lt;dot:RoleView Roles="Manager"&gt; &lt;tr&gt; &lt;td colspan="3"&gt; &lt;dot:PlaceHolder IncludeInPage={value: ProductId &gt; 0}&gt; &lt;a href={value: $"Products.aspx?productId={ProductId}"}&gt;To product&lt;/a&gt; &lt;/dot:PlaceHolder&gt; &lt;dot:PlaceHolder IncludeInPage={value: CategoryId &gt; 0 &amp;&amp; ProductId == 0}&gt; &lt;a href={value: $"Categories.aspx?categoryId={CategoryId}"}&gt;To category&lt;/a&gt; &lt;/dot:PlaceHolder&gt; &lt;dot:PlaceHolder IncludeInPage={value: CategoryId == 0 &amp;&amp; ProductId == 0}&gt; &lt;a href="Dashboard.aspx"&gt;To Dashboard&lt;/a&gt; &lt;/dot:PlaceHolder&gt; &lt;/td&gt; &lt;/tr&gt; &lt;/dot:RoleView&gt; &lt;tr&gt; &lt;th&gt;Date&lt;/th&gt; &lt;th&gt;User&lt;/th&gt; &lt;th&gt;Comment&lt;/th&gt; &lt;/tr&gt; &lt;dot:PlaceHolder IncludeInPage={value: Notes == null || Notes.Count == 0 }&gt; &lt;tr&gt; &lt;td colspan="3"&gt; No notes entered &lt;/td&gt; &lt;/tr&gt; &lt;/dot:PlaceHolder&gt; &lt;dot:Repeater DataSource={value: Notes } RenderWrapperTag="false"&gt; &lt;tr&gt; &lt;td&gt; &lt;dot:Literal Text={value: CreateDate} FormatString="MM/dd/yyyy hh:mm" /&gt;&lt;/td&gt; &lt;td&gt; &lt;dot:Literal Text={value: UserName} /&gt;&lt;/td&gt; &lt;td&gt; &lt;dot:Literal Text={value: Text} /&gt;&lt;/td&gt; &lt;/tr&gt; &lt;/dot:Repeater&gt; &lt;/tbody&gt; &lt;/table&gt; &lt;/div&gt;</code></pre> <p>After migrating few pages, you will get the process semi-automated. I wish you good luck migrating your legacy pages, and I will continue sharing our experience from Web Forms to DotVVM project migrations in the next parts of this series.</p>Thu, 18 May 2023 08:00:00 ZMaxim DužijMaxim.Duzij@riganti.czUsing Composite Controls for the implementation of Bootstrap 5 components<p>You may already be aware that <strong>DotVVM 4.0</strong> has introduced a new feature called <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/control-development/composite-controls"><strong>Composite Controls</strong></a>, which provides a new way to write controls in DotVVM. This approach eliminates the need to declare DotVVM properties in code-only controls, for which you had to use the <strong>dotprop</strong> code snippet and work with long property declaration blocks. Instead, it only requires declaring a `<strong>GetContents</strong>` method and placing individual control properties as arguments of the method. By using this approach, the control code appears much more attractive and readable. </p><p>For instance, consider the following example of a Bootstrap <a href="https://www.dotvvm.com/docs/4.0/controls/bootstrap5/Badge"><strong>Badge</strong></a> control:</p><pre class="brush:csharp"><code class="language-CSHARP">public class Badge : BootstrapCompositeControl { public static DotvvmControl GetContents( HtmlCapability htmlCapability, TextOrContentCapability textOrContentCapability, [DefaultValue(BadgeColor.Primary)] ValueOrBinding&lt;BadgeColor&gt; type, BadgeVisualStyle visualStyle = BadgeVisualStyle.Default, ICommandBinding? click = null ) { return new HtmlGenericControl("span", textOrContentCapability, htmlCapability) .AddCssClass($"badge {visualStyle.ToEnumString()}") .AddCssClass(type.Select(t =&gt; t.ToEnumString())) .SetProperty(Events.ClickProperty, click); } }</code></pre><p> Right away, we can observe a few new types introduced by the composite control approach. The first argument type is <strong>HtmlCapability</strong>, which is a collection of properties utilized across multiple controls. DotVVM provide some composite control capabilities out of the box, such as <strong>HtmlCapability</strong> (containing common properties such as <strong>ID</strong>, <strong>Class-*</strong>, <strong>Style-*</strong>, or <strong>Visible</strong>) and <strong>TextOrContentCapability</strong> (which renders either <strong>Text</strong> (string-only value or binding) or <strong>Content</strong> properties (any hierarchy of HTML and other controls specified as inner elements). It also throws the <strong>DotvvmControlException</strong> when both of these properties are set – this helps to ensure that the syntax is clear and consistent.</p><p>The greatest benefit we learned is possible to do with your own capabilities to share logic between multiple controls. To achieve this, such capability classes must be marked with the <strong>DotvvmControlCapability</strong> attribute and be <strong>sealed</strong>. Since the classes are sealed, you cannot use inheritance, but you can easily include the child capability as one of the properties. </p><p>To illustrate, consider this example of <strong>CheckBoxCapability</strong> and the inner <strong>CheckBoxCoreCapability</strong>: </p><p><pre class="brush:csharp"><code class="language-CSHARP">[DotvvmControlCapability()] public sealed record CheckBoxCapability { public CheckBoxCoreCapability CoreCapability { get; init; } = null!; [DefaultValue(null)] public IValueBinding&lt;IEnumerable&gt;? CheckedItems { get; init; } public CheckBox ToControls() { return CoreCapability.ToControls() .SetProperty(a =&gt; a.CheckedItems, CheckedItems); } }</code></pre><p> <br> As you can see, the child capability is reused in the <strong>ToControl</strong> method. In many capabilities, we’ve created a convention of declaring a <strong>ToControl</strong>/<strong>ToControls</strong> method which builds the corresponding UI from the capability properties, which helps to encapsulate various pieces of the presentation logic. </p><p>The inner <strong>CheckBoxCoreCapability </strong>can be reused in multiple components – for example <a href="https://www.dotvvm.com/docs/4.0/controls/bootstrap5/ButtonGroupCheckBox">ButtonGroupCheckBox</a>, <a href="https://www.dotvvm.com/docs/4.0/controls/bootstrap5/FormControlCheckBox">FormControlCheckBox</a>, and others. The <strong>CheckBoxCapability</strong> is used only in the <a href="https://www.dotvvm.com/docs/4.0/controls/bootstrap5/CheckBox">CheckBox</a> control as it adds several things which are relevant to the CheckBox control only.</p><p><br></p><h2>Dealing with enums</h2><p> Let's revisit the sample <strong>Badge</strong> control. Its next property is a new generic type called <strong>ValueOrBinding&lt;T&gt;</strong>. When the control is used in DotHTML files, the <strong>Type</strong> property can allow either a hard-coded value of the <strong>BadgeColor</strong> type (<em>&lt;bs:Badge Type={value: BadgeColor.Blue} /&gt;</em>), or a data-binding from a view model (<em>&lt;bs:Badge Type={value: _root.MyBadgeColorProperty} /&gt;</em>). </p><p>It’s also possible to allow only either hardcoded values (with composite control property of type <strong>BadgeColor</strong>) or binding only (with composite control property of type <strong>IValueBinding&lt;BadgeColor&gt;</strong>).</p><div><br> When it comes to handling enums, specifying an underlying CSS class is very easy with <strong>EnumMember</strong> attribute. Consider this example of a <strong>BadgeColor</strong> enum:<br> <pre class="brush:csharp"><code class="language-CSHARP">public enum BadgeColor { [EnumMember(Value = "bg-primary")] Primary, [EnumMember(Value = "bg-secondary")] Secondary }</code></pre><p> The <strong>ToEnumString()</strong> extension method converts the <strong>EnumMember</strong> value to a specified string CSS class.</p><blockquote><p>The properties of type <strong>ValueOrBinding&lt;T&gt;</strong> can use the Select extension method to transform the value or binding expression to something else: <em>type.Select(t =&gt; t.ToEnumString())</em>&nbsp;</p></blockquote><p>Using of enums has another benefit – <a href="https://www.dotvvm.com/products/visual-studio-extensions">the DotVVM for Visual Studio extension</a> automatically infers the allowed values in IntelliSense. </p><h2>Limitations</h2><p> The composite control approach of course has its limitations, as we discovered the hard way during the development of the Bootstrap 5 control pack. </p><h3>Exclude a property</h3><p> If a property defined in a capability needs to be excluded in a specific control for some reason, there are a few things that can be done. One approach is to create a new capability without the corresponding property and move the capability logic to a child capability.<br> </p><p>Another approach is faster but less elegant. Even when using the composite approach to write code-only controls, properties can still be specified using the <strong>dotprop</strong> snippet. Then, a <strong>MarkupOptions</strong> attribute with a mapping mode of <strong>Exclude</strong> can be added, so the property is ignored. In the code bellow, the <strong>Style</strong> property is excluded.</p><div><pre class="brush:csharp"><code class="language-CSHARP">public class CardImage : BootstrapCompositeControl, ICardItem { [MarkupOptions(MappingMode = MappingMode.Exclude)] public CardImageStyle Style { get { return (CardImageStyle)GetValue(StyleProperty)!; } set { SetValue(StyleProperty, value); } } public static readonly DotvvmProperty StyleProperty = DotvvmProperty.Register&lt;CardImageStyle, CardImage&gt;(c =&gt; c.Style, CardImageStyle.Default); public IEnumerable&lt;DotvvmControl&gt; GetContents( HtmlCapability htmlCapability, ValueOrBinding&lt;string&gt;? alternateText, ValueOrBinding&lt;string&gt; imageUrl, ValueOrBinding&lt;string&gt;? toolTip, [MarkupOptions(AllowBinding = false, MappingMode = MappingMode.InnerElement)] CardBody? overlay = null ) { yield return new HtmlGenericControl("img", htmlCapability) .SetAttribute("src", imageUrl) .SetAttribute("alt", alternateText) .SetAttribute("title", toolTip); if (overlay is not null) { yield return overlay; } } }</code></pre><h3> Composite Controls Cannot Contain ContentPlaceHolder</h3><p> Keep in mind, that composite controls cannot contain <strong>ContentPlaceHolder</strong> controls, as the content for the master pages are resolved before the <strong>Load </strong>phase when the composite controls call their <strong>GetContents </strong>method.</p><p> For this reason, layout-like controls such as <a href="https://www.dotvvm.com/docs/4.0/controls/bootstrap5/Container">Container</a> in Bootstrap 5 were written in an old-fashioned way. </p><h3>Throwing a DotvvmControlException requires an instance</h3><p> By default, the <strong>GetContents</strong> method should be static and can return either a collection of controls, or a single <strong>DotvvmControl </strong>instance. In case you want to throw an exception from your control, the standard way is to use the <strong>DotvvmControlException</strong>. However, the <strong>DotvvmControlException</strong> has an optional argument of type <strong>DotvvmBindableObject</strong>, and if you provide it, the error page highlights the specific line of code in the view if the control throws the exception.</p><p> To pass this agrument, the <strong>GetContents</strong> method should be then changed to instance method:</p><pre class="brush:csharp"><code class="language-CSHARP">public DotvvmControl GetContents(HtmlCapability htmlCapability, DropDownCapability capability) { if (!capability.IsSplitButton) { throw new DotvvmControlException(this, "Cannot set parent reference."); } }</code></pre><h2>Thinking is a composite way </h2><p>The new composite control approach encourages a different way of thinking. By reusing capabilities and helper methods, the way of how can we build a DOM tree is much more versatile. Things like validation of a specific properties, that work together can be shifted to the separate capability logic and aligned with a different controls scenarios.<br> </p><p>The Fluent API methods like <strong>AddCssClass</strong>, <strong>AddAttribute </strong>or <strong>AppendChildren </strong>allow to produce fairly advanced control logic with just a few lines of code.</p><p>If you are interested in more info about the Composite controls, see the recent blog post series:</p><ul><li><a href="https://www.dotvvm.com/blog/89/Part-1-Have-you-tried-the-new-composite-controls-">Part 1: Have you tried the new composite controls?</a></li><li><a href="https://www.dotvvm.com/blog/90/Part-2-Have-you-tried-the-new-composite-controls-">Part 2: Have you tried the new composite controls?</a></li></ul><p><br></p></div></div></p>Tue, 21 Mar 2023 11:49:39 ZTomáš Hercegtomas.herceg@riganti.czReleased DotVVM 4.1<p>After 8 months, we are <strong>thrilled to announce the release of DotVVM 4.1</strong>. The release contains <strong>the new DotVVM Auto UI library</strong>,<strong> </strong>numerous <a href="https://github.com/riganti/dotvvm/releases/tag/v4.1.0">improvements and bug fixes</a>, and a bunch of improvements of Composite controls which allowed us to create a brand new implementation of <a href="https://www.dotvvm.com/products/bootstrap-for-dotvvm"><strong>Bootstrap for DotVVM</strong></a><strong> for Bootstrap 5</strong>.</p><h2>DotVVM Auto UI</h2><p>The idea of auto-generating forms and grids based on model classes and data annotation attributes is not anything new. Previously, we had a package called <a href="https://www.dotvvm.com/docs/4.0/pages/community-add-ons/dotvvm-dynamic-data">DotVVM Dynamic Data</a>, which could do this.</p><p>The problem of Dynamic Data implementation was mainly the performance. On every HTTP request, Dynamic Data components were doing a lot of Reflection calls including compilation of data-bindings. Some things were cached, but a lot of things couldn’t be done properly.</p><p>With <strong>DotVVM 4.1</strong>, we are now publishing a new package <strong>DotVVM.AutoUI</strong>. </p><p>To use the library, you need to register it in the <strong>ConfigureServices</strong> method in <strong>DotvvmStartup.cs</strong>:</p><pre class="brush:csharp"><code class="language-CSHARP">options.AddAutoUI();</code></pre><p>After that, you can use the Auto UI controls in your DotVVM pages. Currently, there are several controls:</p><ul><li><strong>&lt;auto:Editor Property=… /&gt; </strong>will auto-generate editing control for a property you’ll bind to it. If it is a string, it will be a <strong>TextBox</strong>. For booleans, it will be <strong>CheckBox</strong>, and so on.</li><li><strong>&lt;auto:Form /&gt;</strong> comes in two versions – the most popular will be <strong>&lt;auto:BootstrapForm /&gt;</strong>. It will look at the <strong>DataContext </strong>property of itself or of its closest ancestor, and generate a complete form for all properties defined in the data context object. The field labels and other metadata will be provided using Data annotation attributes.</li><li><strong>&lt;auto:GridViewColumns /&gt;</strong> is a special type of column which can be used in the <strong>GridView </strong>control. It will generate columns for all properties of the objects bound to the <strong>DataSource</strong>.</li></ul><p>If you want to generate a simple login form, you will need to decorate your model class with a few attributes:</p><pre class="brush:csharp"><code class="language-CSHARP">public class SignInModel { [Display(Name = "User Name")] [Required(ErrorMessage = "User name is required!")] public string UserName { get; set; } = ""; [Display(Name = "Password")] [Required(ErrorMessage = "Password is required!")] [DataType(DataType.Password)] public string Password { get; set; } = ""; [Display(Name = "Remember sign in")] public bool RememberMe { get; set; } }</code></pre><p>The <strong>BootstrapForm</strong> control will do the rest, including the validation:</p><p><img width="458" height="248" title="Auto-generated form in Bootstrap style" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="Auto-generated form in Bootstrap style" src="https://dotvvmstorage.blob.core.windows.net/blogpost/061979ed-707d-4679-97b6-8c2351b0d6a3.png" border="0"></p><p>Since it can be boring to decorate every field with the <strong>Display </strong>attribute, <strong>Auto UI </strong>supports also providing the field labels using a RESX file. The keys are the property names and the values are the localized texts for each culture.</p><p>If you have been using Dynamic Data, the migration should be quite easy – just the control names are different, but their usage is similar. Auto UI has also several new features which weren’t present in Dynamic Data.</p><h3>Selections</h3><p>Often, you need the user to select some value from a list. Auto UI brings the <strong>Selection </strong>attribute which defines the type representing the set from the user is selecting. </p><pre class="brush:csharp"><code class="language-CSHARP">// add the Selection attribute on the property [Selection(typeof(CountrySelection))] public int? CountryId { get; set; }</code></pre><p>You need to implement an <strong>ISelectionProvider&lt;YourType&gt; </strong>to provide values for the selection.</p><pre class="brush:csharp"><code class="language-CSHARP">public record CountrySelection : Selection&lt;int&gt;; public class CountrySelectionProvider : ISelectionProvider&lt;CountrySelection&gt; { private readonly AppDbContext dbContext; public CountrySelectionProvider(AppDbContext dbContext) { this.dbContext = dbContext; } public Task&lt;List&lt;CountrySelection&gt;&gt; GetSelectorItems() { return dbContext.Countries .OrderBy(c =&gt; c.Name) .Select(c =&gt; new CountrySelection() { Value = c.Id, DisplayName = c.Name }) .ToListAsync(); } }</code></pre><p>Finally, include the property of type <strong>SelectionViewModel&lt;YourType&gt; </strong>in the page viewmodel and initialize it. The rest will happen automatically.</p><pre class="brush:csharp"><code class="language-CSHARP">// add this to the page viewmodel public SelectionViewModel&lt;CountrySelection&gt; Countries { get; set; } = new();</code></pre><pre class="brush:csharp"><img width="1024" height="423" title="ComboBox generated using the Selection attribute" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="ComboBox generated using the Selection attribute" src="https://dotvvmstorage.blob.core.windows.net/blogpost/1479e31b-5c80-47aa-a9c2-d17924673e7f.png" border="0"></pre><p>Note that this will also work for collections – if you want to allow the user to select multiple items, use <strong>List&lt;int&gt; </strong>in your model class. Auto UI will generate a list of checkboxes.</p><p><img width="415" height="169" title="image" style="margin: 0px; border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="image" src="https://dotvvmstorage.blob.core.windows.net/blogpost/5465b713-6a79-4aeb-a841-9359884c4ff1.png" border="0"></p><h3>Customization of field editors</h3><p>Another feature which wasn’t really possible in previous Dynamic Data implementation, was to cover exceptions with custom field templates, and respond to changes to the auto-generated fields. Often, you can generate the entire form except for one property with some special behavior.</p><p>This can be covered by property groups of the <strong>&lt;auto:Editor&gt;</strong> and <strong>&lt;auto:GridViewColumns&gt;</strong>:</p><pre class="brush:dothtml"><code class="language-DOTHTML">&lt;auto:BootstrapForm DataContext="{value: Meetup}" Changed-CountryId="{command: _root.OnCountryChanged()}"&gt; &lt;EditorTemplate-ImageUrl&gt; &lt;img src="{value: ImageUrl}" Visible="{value: ImageUrl != null}" class="img-thumbnail" /&gt; &lt;dot:FileUpload UploadedFiles="{value: _root.ImageUpload}" UploadCompleted="{command: _root.ProcessImage()}" /&gt; &lt;/EditorTemplate-ImageUrl&gt; &lt;/auto:BootstrapForm&gt;</code></pre><p>The <strong>Changed-<em>PropertyName</em> </strong>property allows to hook on the <strong>Changed</strong> (or equivalent) event of auto-generated controls. The <strong>EditorTemplate-<em>PropertyName</em> </strong>allows to provide a custom editor – in our example we are giving the user a <strong>FileUpload </strong>control to upload the profile image, instead of generating just a <strong>TextBox </strong>(because <strong>ImageUrl </strong>is a <strong>string </strong>property).</p><h3>Configuration fluent API</h3><p>The last but not least feature of DotVVM Auto UI is the option to use conventions instead of placing data annotation attributes to every property in every model class. </p><pre class="brush:csharp"><code class="language-CSHARP">options.AddAutoUI(options =&gt; { options.PropertyMetadataRules .For(property =&gt; property.Name.EndsWith("Price"), rule =&gt; rule.SetFormatString("n2")); });</code></pre><p>This will use the two decimal places number format for all properties with name ending with the word “Price”. If your model follows good naming conventions, you can do a lot of magic with this.</p><blockquote><p>You can find more info in the <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/auto-ui/overview">the new Auto UI chapter in DotVVM documentation</a>.</p></blockquote><p><br></p><h2>Markup declared properties</h2><p>If you ever wrote your own <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/control-development/markup-controls-with-code">markup control</a> and wanted to add properties to is, you probably know that it requires creating a C# class and invoking the crazy <em>dotprop </em>code snippet.</p><font face="Courier New"></font><p>Starting with DotVVM 4.1, you can now declare the properties using the <strong>@property </strong>directive at the top of the file. You can include a default value, and also specify the attributes of the property.</p><pre class="brush:dothtml"><code class="language-DOTHTML">@property string Title = "Panel" @property List&lt;CategoryModel&gt; Categories, MarkupOptionsAttribute.AllowHardCodedValue = false</code></pre><p>See <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/control-development/markup-controls#declare-the-property-using-the-property-directive">all options of markup declared properties</a> here.</p><p><br></p><h2>Stable version of React components in DotVVM</h2><p>DotVVM 4.0 introduced an experimental support of hosting React components in DotVVM pages. In DotVVM 4.1 we’ve fixed many issues and marked this feature as stable. </p><p>You can now use React components in your DotVVM pages, bind the viewmodel data into their properties, and respond to the events which happen in the control.</p><pre class="brush:dothtml"><code class="language-DOTHTML">&lt;js:Recharts data={value: Data} onMouse={staticCommand: (string n) =&gt; CurrentThing = n}&gt; &lt;/js:Rrecharts&gt;</code></pre><p>The control is exported from a JS module which you import in the page using the <strong>@js </strong>directive.</p><pre class="brush:js"><code class="language-JS">import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { registerReactControl } from 'dotvvm-jscomponent-react'; import { LineChart, XAxis, Tooltip, CartesianGrid, Line } from 'recharts'; // react component function RechartComponent(props: any) { return ( &lt;LineChart width={400} height={400} data={props.data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }} &gt; &lt;XAxis dataKey="name" /&gt; &lt;Tooltip /&gt; &lt;CartesianGrid stroke="#f5f5f5" /&gt; { Object.keys(props.data[0]).slice(1).map((s, i) =&gt; &lt;Line type="monotone" dataKey={s} stroke={"#" + (i * 4).toString() + "87908"} yAxisId={i} onMouseEnter={(_) =&gt; props.onMouse(s)} /&gt;) } &lt;/LineChart&gt; ); } export default (context: any) =&gt; ({ $controls: { Recharts: registerReactControl(RechartComponent, { context, onMouse() { /* default empty method */ } }) } })</code></pre><p>The <strong>RechartComponent </strong>function builds the <strong>LineChart </strong>component from the popular <a href="https://recharts.org/en-US/">Recharts</a> React library. It will pass the <strong>props.data</strong> collection as the chart data, and call the <strong>props.onMouse</strong> function whenever the user hovers on some series in the chart.</p><p>The module exports a factory for the JS module instance, and this instance contains the <strong>$controls </strong>object. The <strong>Recharts </strong>key in this object will be published as <strong>&lt;js:Recharts&gt; </strong>component in the DotVVM page. You just need to wrap the <strong>RechartComponent </strong>function with the <strong>registerReactControl – this is the integration bridge between DotVVM and React.</strong></p><p>We have a similar mechanism for Svelte in progress, and it is not difficult to support other JS frameworks in the future.</p><p><br></p><h2></h2><h2>Bootstrap for DotVVM support of Bootstrap 5</h2><p>Our <a href="https://www.dotvvm.com/products/bootstrap-for-dotvvm"><strong>Bootstrap for DotVVM</strong></a><strong> </strong>commercial packages supported Bootstrap 3 and 4, and we are finally adding the support for <strong>Bootstrap 5</strong>. The reason why it took us so long was that we wanted to implement the controls using the new Composite controls approach introduced in DotVVM 4.0. During the implementation we found a lot of things to be improved, and it required delaying the release until now.</p><p>Supporting the next version of Bootstrap will be much easier – the composite controls are much cleaner and much better maintainable than the classic approach of writing controls. Also, we heavily utilized the <a href="https://www.dotvvm.com/blog/90/Part-2-Have-you-tried-the-new-composite-controls-">Control capabilities</a> because some Bootstrap components contain very similar properties, or have different versions for various purposes (e. g. CheckBox used in the InputGroup renders a bit different output and some of its properties don’t make much sense). Instead of using inheritance which has many limits, we can build the controls by using multiple capabilities which work similarly as mixins. </p><p>If you have purchased a <strong>license for Bootstrap for DotVVM </strong>in the last 12 months, you should be able to see the new package <strong>DotVVM.Controls.Bootstrap5 </strong>on the <a href="https://www.dotvvm.com/docs/4.0/pages/dotvvm-for-visual-studio/dotvvm-private-nuget-feed">DotVVM NuGet Feed</a><strong> </strong>immediately. If you purchased the license earlier, you will need to renew it to get the new release (and you’ll get 12 months of new versions, improvements and bug fixes as a part of the license).</p><p id="business-pack"><em>Since this is a brand new library, we expect issues. If you find any glitch or unwanted behavior, please let us know – we’ll publish a fix immediately. And check for the new versions – we expect several revision releases to be published within the next weeks.</em></p><p><br></p><h2>DotVVM Business Pack</h2><p>There were several infrastructure improvements in <strong>DotVVM Business Pack </strong>as well. </p><p>The most visible change is that we’ve split the CSS and JS bundle into smaller modules. Thanks to that, the pages using the Business Pack controls will load faster and download less resources.</p><p>Here is a comparison for a page using GridView, Button, DataPager and a TextBox:</p><p><img width="789" height="175" title="DotVVM 4.0 needs 465 kB of JS and 98 kB of CSS; DotVVM 4.1 needs just 90 kB of JS and 25 kB of CSS for the example page" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="DotVVM 4.0 needs 465 kB of JS and 98 kB of CSS; DotVVM 4.1 needs just 90 kB of JS and 25 kB of CSS for the example page" src="https://dotvvmstorage.blob.core.windows.net/blogpost/1d7485f3-e768-4a58-ae1f-1ebd61dc75b6.png" border="0"></p><p><br></p><p>For the next release, we plan several major improvements we have already started working on:</p><ul><li><strong>GridView cell edit mode </strong>– when enabled, it will allow the users to change values in the individual cells (like in Excel) and give you events to respond to these edits.</li><li><strong>Grouping in GridView </strong>– we have struggled to find a good way of how to approach grouping so it doesn’t collide with paging and other features. Now we think we found it, and it will be available soon.</li><li><strong>Responsive styles and behavior </strong>for <strong>ComboBox</strong>, <strong>DropDownList</strong>, <strong>MultiSelect</strong> and <strong>Date</strong>/<strong>Time</strong>/<strong>ColorPicker</strong> controls – currently they have some issues on mobile and touch screens.</li></ul><p><br></p><h2>Summary</h2><p>I’d like to personally thank the team and all contributors from the community. Thanks to your feedback, bug reports and code contributions, we were able to release this large bag of features. The full list of changes in in the <a href="https://github.com/riganti/dotvvm/releases/tag/v4.1.0"><strong>Release notes</strong></a>.</p><p>We’d be happy to hear your feedback. Join the discussion on our <a href="https://gitter.im/riganti/dotvvm"><strong>Gitter chat</strong></a>!</p>Mon, 06 Feb 2023 16:56:30 ZTomáš Hercegtomas.herceg@riganti.czIntroducing DotVVM Application Blocks<p><strong>DotVVM </strong>was always focused on improving productivity for .NET developers who build web applications. <p>DotVVM 1.0 was introduced at the time of the .NET Framework and the growing popularity of client-side frameworks like Angular JS, React, and Knockout JS. DotVVM offered to build web apps with only C# and HTML knowledge. Soon after that, we released DotVVM 1.1 with support for .NET Core. <p>In subsequent releases, we have been adding plenty of new features. DotVVM 2.0 added static commands, which could patch parts of the viewmodel instead of sending it to the server and back. Also, REST API bindings provided a direct way to bind data on the page from any API.<p>DotVVM 3.0 introduced the JS directive allowing you to interact with JavaScript code easily, and in DotVVM 4.0, we added React integration which allows hosting third-party components.<p>Compared with client-side JavaScript frameworks, DotVVM saves significant development time. However, many users prefer Blazor, which also offers writing web apps with just C# and HTML, and offers similar simplicity and productivity.<p>That's why we decided to move forward and introduce an even more productive way of building modern web apps. As the blog post title suggests, the name is <strong>DotVVM Application Blocks</strong>.<h2>Building apps from ready-made blocks</h2><p>A lot of business websites and portals consist of similar parts. There is some kind of sign-in and sign-up experience. There are often lots of GridView pages with sorting and filtering capabilities. There is also plenty of forms for inserting and editing data, often with quite complex interactivity and business logic.<p>What if you can just use these high-level building blocks and compose your application from them? <p>Each application block contains not only from the presentation layer things (visual components, page templates), but it contains also business logic. The business logic contains a lot of extensibility points so you are free to change the behavior, provide your own data source, or develop a universal extension you can use on multiple pages.<p>It looks like a “low-code” platform, but you are still a developer using Visual Studio, C# and Git. And you are not limited by the feature set of the framework – you can always write any part of the application yourself.<h2>List and detail page</h2><p>The only thing you will need for an app using Application Blocks, is the <strong>Program.cs</strong> file. You can configure almost anything using a Fluent API syntax. </p><pre class="brush:csharp"><code class="language-CSHARP">builder.AddApplicationBlocks(blocks =&gt; { // set up data-access for Order entity using Entity Framework Core blocks.AddEfCoreDataSource&lt;AppDbContext, Order&gt;(); // use an application template based on Bootstrap blocks.UseBootstrap5Template(); // register pages blocks .AddListPage&lt;OrderListModel, Order&gt;(page =&gt; { page.BeginPageExtensions.AddFullTextSearch(); }) .AddDetailPage&lt;OrderDetailModel, Order&gt;(page =&gt; { page.UseLayout(layout =&gt; layout.Columns( layout.Form(m =&gt; m.Name, m =&gt; m.Price), layout.Form(m =&gt; m.DepartmentId) )); }); });</code></pre><p>You can see that the code installed data-source for the <strong>Order </strong>entity – it is using Entity Framework Core in the background, but the infrastructure allows us to build providers for other data source as well. We plan to introduce a provider for Azure Cosmos DB, Mongo DB and Marten.</p><p>You can see that the page template is also customizable. We plan to ship <strong>Bootstrap 5 </strong>and <strong>DotVVM Business Pack </strong>templates, but you’ll be able to change or build the templates to your own preference. </p><p>Finally, the code registers a list page and a detail page. The list page renders a <strong>GridView </strong>with sorting, and we configure it to add a full-text search extension. It will take all string fields in the GridView and allow searching on them.</p><p>The detail page defines a two-columns layout and lists which fields should be in which column. There will be more ways to do this, using <strong>DotVVM Auto UI </strong>and annotation attributes.</p><p>You can see that each page also has its own model – OrderListModel and OrderDetailModel. That’s because each of the pages may need slightly different fields with different configuration. The framework will provide an automatic way how to map the <strong>Order </strong>entity to these models, also with an.</p><p><img width="860" height="460" src="https://www.dotvvm.com/wwwroot/Images/products/application_blocks.jpeg"></p><h2>Dashboard with tiles</h2><p>Another common requirement is to be able to quickly define dashboards with aggregated values and charts. This API will contain many extensibility points, and of course building your own tiles would be supported. However, for creating a quick aggregations for total values, or values filtered by some period of time, this can save a lot of time.</p><pre class="brush:csharp"><code class="language-CSHARP">blocks .AddDashboardPage(page =&gt; { // total price of all orders page.Tiles.AddValueTile&lt;Order&gt;(LocalizableString.Constant("Orders"), o =&gt; (double)o.Price); // chart total price of orders by department page.Tiles.AddChartTile&lt;Order, string&gt;(LocalizableString.Constant("Departments"), o =&gt; (double)o.Price, o =&gt; o.Department.Name) })</code></pre><pre class="brush:csharp"><img width="860" height="460" title="image" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="image" src="https://dotvvmstorage.blob.core.windows.net/blogpost/ceb13245-538d-4363-933e-d507e8e77906.png" border="0"></pre><h2>Menu and navigation</h2><p>All pages will be automatically registered in DotVVM route table. The Application Blocks templates will provide their own master pages which will render the entire menu. </p><h2>Extensibility</h2><p>Each type of page will provide placeholders where you can place “extensions”. You can image an extension as a markup control which can communicate with the page viewmodel and respond to its events. </p><p>In our prototype, we have implemented the FullTextSearch extension which you can add to a list page and it can modify the filters before the data is loaded. Similarly, we’ll have extensions that can be added on detail pages – e. g. comments section, tags, uploading attachments, browsing the history of changes, and more.</p><h2>Sign up for a early preview</h2><p>We plan to launch an early preview during February. <strong>If you are interested, </strong><a href="https://www.dotvvm.com/app-blocks-preview"><strong>sign up</strong></a><strong> for an early access</strong>. We are looking forward to share more info with you in the upcoming weeks, and thrilled to hear your feedback.</p>Tue, 17 Jan 2023 13:52:46 ZTomáš Hercegtomas.herceg@riganti.czDotVVM Tip 09: Bind multiple CheckBoxes to a collection<blockquote><p><em>DotVVM Tips is a series of short articles showing interesting features of DotVVM. To learn more, visit our </em><a href="https://www.dotvvm.com/docs/4.0/pages/introduction"><em>Docs site</em></a><em>.</em></p><p><em><br></em></p></blockquote><p>Building forms is really easy! To put all selected options into a collection, just bind the collection to <a href="https://www.dotvvm.com/docs/4.0/controls/builtin/CheckBox"><strong>multiple CheckBox controls</strong></a>. </p><pre class="brush:dothtml"><code class="language-DOTHTML">// DotVVM view &lt;dot:CheckBox Text="Apple" CheckedItems="{value: FavoriteFruit}" CheckedValue="Apple" /&gt; &lt;dot:CheckBox Text="Orange" CheckedItems="{value: FavoriteFruit}" CheckedValue="Orange" /&gt; &lt;dot:CheckBox Text="Banana" CheckedItems="{value: FavoriteFruit}" CheckedValue="Banana" /&gt;</code></pre><pre class="brush:csharp"><code class="language-CSHARP">// DotVVM viewmodel public List&lt;string&gt; FavoriteFruit { get; set; } = new();</code></pre><p>The <strong>CheckedItems </strong>property points to a collection. The <strong>CheckedValue</strong> tells the control what value will be placed in the collection if the <strong>CheckBox</strong> is checked. As a result, you’ll have the selected identifiers in the collection.</p><p>The data-binding works in both ways – if you modify the collection, the <strong>CheckBox</strong> controls will update their state to match the values in the collection.</p>Mon, 12 Dec 2022 13:22:00 ZTomáš Hercegtomas.herceg@riganti.czUsing web components with composite controls in DotVVM<p>One of the scenarios in which the <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/control-development/composite-controls"><strong>new way of building controls in DotVVM – the composite controls</strong></a><strong>&nbsp;</strong>– really excel, is when used <strong>in combination with </strong><a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components"><strong>Web components</strong></a>.</p><p><strong>Web components </strong>is a great concept which allows to define custom HTML elements and define how they’ll look in the page thanks to attaching the Shadow DOM (HTML that will be rendered but stands aside of the page DOM).</p><p><img width="508" height="197" title="Web component with its shadow DOM in Dev Tools" style="border: 0px currentcolor; border-image: none; display: inline; background-image: none;" alt="Web component with its shadow DOM in Dev Tools" src="https://dotvvmstorage.blob.core.windows.net/blogpost/3d7c9433-9efc-4be8-b309-a66595763102.png" border="0"></p><p>In the picture above, you can see a custom element <strong>&lt;fluent-button&gt;</strong>. This component can have its own attributes<strong>&nbsp;</strong>(e. g. <strong>current-value</strong>), and can produce the Shadow DOM which will be rendered. However, in the main page DOM, there is still just one element <strong>&lt;fluent-button&gt;</strong> instead of the <strong>&lt;button&gt; </strong>element and the <strong>&lt;span&gt;</strong> elements inside.</p><p>There are several component libraries based on Web Components – for example <a href="https://github.com/material-components/material-web">Material Design</a>, <a href="https://github.com/vaadin/web-components">vaadin</a>, <a href="https://github.com/github/github-elements">GitHub elements</a>, and more. Recently, Microsoft announced porting their <a href="https://github.com/microsoft/fluentui/tree/master/packages/web-components">Fluent UI into Web Components</a>. We were quite passionate about that, however the project now looks a bit abandoned and there haven’t been many new commits recently – it seems that Microsoft is focusing on the React version of the controls.</p><p><br></p><h2>Data-binding to web components</h2><p>It is quite easy to use Web Components in DotVVM projects since they are using only the primitives offered by HTML and DOM manipulation.</p><p>For example, to render a Progress component from Fluent UI web components, you can use the following snippet. You can use data-binding to set attribute values, and it will just work:</p><pre class="brush:dothtml"><code class="language-DOTHTML">&lt;fluent-progress min="0" max="100" value="{value: Progress}"&gt;&lt;/fluent-progress&gt;</code></pre><p>The values of HTML attributes are always treated as strings, but DotVVM will do a conversion of numeric and date-time values for you automatically – you don’t need to do anything special.</p><h3>Two-way binding</h3><p>A more interesting situation will occur when you need to make the data-binding working both ways. By default, HTML doesn’t let you know when a value of attribute changes. Even the default HTML elements only have events to notify about changes on some of their attributes. For example, the <strong>&lt;input&gt; </strong>element triggers the <strong>onchange</strong> event when its value is changed, and it applies only to the situation when the user makes the change. If you change the <strong>value</strong> attribute programmatically, the <strong>onchange</strong> event is not going to be triggered.</p><p>When you use data-binding on a HTML attribute, DotVVM will generate something like this:</p><pre class="brush:dothtml"><code class="language-DOTHTML">&lt;fluent-progress data-bind="attr: { value: Progress }" ... /&gt;</code></pre><p>This Knockout JS binding handler sets a subscription on the <strong>Progress </strong>property in the viewmodel, and whenever its value changes, it will update the HTML attribute value. It doesn’t try to do the binding the opposite way since there are no good events for that.</p><p>If you are binding specifically the <strong>value</strong> attribute and the element has the <strong>onchange</strong> event, you may use the Knockout JS <strong>value</strong> binding which will cover the way back. </p><p>If you are working with a component library, there may be some generic mechanism to handle changes of attributes. For example, the <em>Fluent UI Web Components</em> have the <strong>Observable </strong>class that can do this. The easiest way to approach this is to write your own Knockout binding handler which will work similar to the <strong>attr</strong> binding but will take care of the way back:</p><pre class="brush:js"><code class="language-JS">import { Observable } from '@microsoft/fast-element'; export function init() { ko.bindingHandlers["fast-bind"] = { init(element, valueAccessor, allBindings, viewModel, bindingContext) { const notifier = Observable.getNotifier(element); const value = ko.unwrap(valueAccessor()) || {}; ko.utils.objectForEach(value, function (attrName, attrValue) { notifier.subscribe({ handleChange(source, propertyName) { const expr = (valueAccessor() || {})[attrName]; if (ko.isWritableObservable(expr)) { expr(source[propertyName]); } } }, attrName); }); }, update(element, valueAccessor, allBindings, viewModel, bindingContext) { const value = ko.unwrap(valueAccessor()) || {}; ko.utils.objectForEach(value, function (attrName, attrValue) { attrValue = ko.utils.unwrapObservable(attrValue); element[attrName] = attrValue; }); } } }</code></pre><p>The Knockout binding handler has two methods – <strong>init</strong> and <strong>update</strong>. </p><p>The <strong>init</strong> method is called as soon as the HTML element appears in the page. In most cases, it’s when the page is loaded, but remember that elements may appear in a page later – for example in SPAs, or when you add an item in a collection bound to Repeater – in that moment, a new elements appear in the page. The purpose of this method is mainly to hook up on various events and set up handlers for them.</p><p>The <strong>update</strong> method is called right after <strong>init</strong>, and also whenever the binding value is updated. In this method, you should read the value from the viewmodel, and apply it on the HTML element. </p><p>In our case, the binding value may be an object with several properties – we are trying to make the binding similar to <strong>attr</strong>:</p><pre class="brush:dothtml"><code class="language-DOTHTML">&lt;fluent-progress data-bind="fast-bind: { value: Progress }" ... /&gt;</code></pre><p>You can see that in the <strong>init</strong> method we’ll get a notifier object for the element (this object is a part of Fluent UI library). Then, we are evaluating <strong>valueAccessor()</strong> which is a function which will give us the value of the binding: <strong>{ value: Progress }</strong></p><p>After we have it, we’ll go through all keys in the object and register a handler for the attribute changes on the notifier object. If any of the attributes will be changed, we’ll look in the corresponding property in the binding value, and if we’ll find a writeable observable there, we’ll set the new value in it. This last check is important as the expression doesn’t have to be writeable – e. g. <strong>{value: ItemsProcessed / ItemsTotal}</strong> would be a valid DotVVM binding, but it is not possible to update these two properties if you know only the result. In such case, the <strong>ko.isWritableObservable</strong> will return <strong>false</strong> and we’ll do nothing.</p><p>The <strong>update</strong> method just goes through all the properties in the object, reads the values of individual observables and sets them to the attributes. This will register handlers on all observables automatically – Knockout JS tracks which observables you had to evaluate in your <strong>update</strong> handler, and will call <strong>update</strong> when any of these observables will change.</p><h3>Collections of elements</h3><p>Some web components contain child elements. This is often used when you work with collections. In DotVVM, there is the <strong>Repeater </strong>control which can render elements based on items in a viewmodel collection, which is exactly what we need here. You can use <strong>WrapperTagName </strong>property to tell <strong>Repeater </strong>what element should be rendered around the items (by default it’s <strong>&lt;div&gt;</strong>).</p><pre class="brush:dothtml"><code class="language-DOTHTML">&lt;fluent-select&gt; &lt;fluent-option value="1"&gt;One&lt;/fluent-option&gt; &lt;fluent-option value="2"&gt;Two&lt;/fluent-option&gt; &lt;fluent-option value="3"&gt;Three&lt;/fluent-option&gt; &lt;fluent-option value="4"&gt;Four&lt;/fluent-option&gt; &lt;fluent-option value="5"&gt;Five&lt;/fluent-option&gt; &lt;/fluent-select&gt; &lt;dot:Repeater DataSource="{value: Items}" WrapperTagName="fluent-select"&gt; &lt;fluent-option value="{value: Value}"&gt;{{value: Name}}&lt;/fluent-option&gt; &lt;/dot:Repeater&gt;</code></pre><p>These two elements will have the same outcome.</p><p><br></p><h2>Composite control as a wrapper</h2><p>Now when we dealt with the primitive things, let’s make our life simpler and provide strongly-typed wrappers for our web components. This is not difficult to do, and it will give you a more precise IntelliSense. Also, you’ll be able to hide a lot of logic inside the controls, which will help to get rid of duplicate code.</p><h3>Rendering the fast-bind binding handler</h3><p>Instead of rendering the <strong>data-bind </strong>attribute with the correct binding handler manually, we can create an <em>attached property </em>which can be set to any control even if the control doesn’t define them. You’ve probably met attached properties when using validation – <strong>Validation.Enabled=false</strong> is an attached property defined in the <strong>Validation</strong> class in the framework. You can apply this property on any HTML element and on most DotVVM controls (those which render exactly one HTML element). </p><p>In our case we’ll need something more – since you need to specify both name of the attribute and its value, and there can be several of them on the same element, we need <em>property group</em>. A property group is a dictionary of properties with the same prefix. You’ve seen them on the <strong>RouteLink</strong> control where you can specify for example <strong>Param-Id</strong> and <strong>Param-Title</strong> properties which specify the values for the <strong>Id</strong> and <strong>Title</strong> route parameters. </p><p>We’ll need to define an attached property group which will be used like this:</p><pre class="brush:dothtml"><code class="language-DOTHTML">&lt;fluent:Select Fast.Bind-value="{value: SelectedValue}" ... /&gt;</code></pre><p>The code where such property group is defined, can look like this:</p><pre class="brush:csharp"><code class="language-CSHARP">[ContainsDotvvmProperties] public class Fast { [AttachedProperty(typeof(object))] [PropertyGroup("Bind-")] public static DotvvmPropertyGroup BindGroupDescriptor = DelegateActionPropertyGroup&lt;object&gt;.Register&lt;Fast&gt;("Bind-", "Bind", AddBindProperty); private static void AddBindProperty(IHtmlWriter writer, IDotvvmRequestContext context, DotvvmPropertyGroup group, DotvvmControl control, IEnumerable&lt;DotvvmProperty&gt; properties) { var bindingGroup = new KnockoutBindingGroup(); foreach (var prop in properties) { bindingGroup.Add(prop.Name.Split(':')[1], control, prop); } writer.AddKnockoutDataBind("fast-bind", bindingGroup); } }</code></pre><p>The class must be marked with <strong>ContainsDotvvmProperties</strong> attribute – DotVVM needs it to be able to discover the property declaration.</p><p>The property is a static field of type <strong>DotvvmPropertyGroup</strong> – it is a descriptor object which contains the metadata of the property – its name, its prefix in the markup, and a handler that will be called when rendering a control with such property. </p><p>The property is marked with <strong>AttachedProperty</strong> so it can be used on any control or HTML element. </p><p>The <strong>AddBindProperty</strong> method is called when such element is rendered. We are creating a <strong>KnockoutBindingGroup</strong> object (an object which represents <strong>{ key: value, key2: value2… }</strong> dictionary in Knockout data-binding. We’ll add all properties in the collection, and call <strong>writer.AddKnockoutDataBind</strong>. It will merge the binding group with other <strong>data-bind</strong> attributes that are already specified on the control, so they’ll be rendered as one attribute.</p><h3>Wrappers</h3><p>In the previous blog posts (<a href="https://www.dotvvm.com/blog/89/Part-1-Have-you-tried-the-new-composite-controls-"><strong>Part 1</strong></a>, <a href="https://www.dotvvm.com/blog/90/Part-2-Have-you-tried-the-new-composite-controls-"><strong>Part 2</strong></a>), we’ve shown how to define composite controls. You can use the <strong>SetProperty</strong> overload which accepts the <strong>DotvvmProperty</strong> object – this way you can use the property group descriptor and ask for a specific property name for the target attribute.</p><pre class="brush:csharp"><code class="language-CSHARP">[ControlMarkupOptions(AllowContent = true)] public class NumberField : CompositeControl { public static DotvvmControl GetContents( HtmlCapability htmlCapability, ValueOrBinding&lt;double&gt;? value ) { return new HtmlGenericControl("fluent-number-field", htmlCapability) .SetProperty(Fast.BindGroupDescriptor.GetDotvvmProperty("value"), value); } protected override void OnPreRender(IDotvvmRequestContext context) { context.ResourceManager.AddRequiredResource(Constants.FluentUIResourceName); base.OnPreRender(context); } }</code></pre><p>In the <strong>PreRender</strong> method, the control just requests the resource with the binding handler. The resource should be configured so they are loaded after DotVVM and Knockout JS.</p><p><br></p><h2>Fluent UI wrappers</h2><p>Occasionally, we are getting asked when we’ll eventually publish the wrappers for Fluent UI Web Components. We are still considering this although we think that the package is not of much use. The components are quite limited in their features, and the repo is not updated very frequently so it is unsure whether the controls will be production ready at some point. Currently, there are only basic components, and even things like calendar or date picker are not in a working state. </p><p>If you have a use case for the <em>Fluent UI Web Components</em>, or have been using some other library based on web components, let us know about your experience. If there is demand for some specific library, we will be happy to build a component package for that.</p>Mon, 05 Dec 2022 14:28:03 ZTomáš Hercegtomas.herceg@riganti.czDotVVM Tip 08: Expressions in data-bindings are allowed<blockquote><p><em>DotVVM Tips is a series of short articles showing interesting features of DotVVM. To learn more, visit our </em><a href="https://www.dotvvm.com/docs/4.0/pages/introduction"><em>Docs site</em></a><em>.</em></p><p><em><br></em></p></blockquote><p>Building forms is really easy! Use the <strong>Visible</strong> property to show or hide forms fields based on values in other fields.</p><pre class="brush:dothtml"><code class="language-DOTHTML">// DotVVM view &lt;dot:CheckBox Text="I want to receive newsletters" Checked="{value: Subscribe}" /&gt; &lt;dot:CheckBox Text="I want to be contacted with additional questions" Checked="{value: ResponseRequired}" /&gt; &lt;dot:TextBox placeholder="Enter your e-mail" Text="{value: EmailAddress}" Visible="{value: Subscribe || ResponseRequired}" /&gt;</code></pre>Mon, 05 Dec 2022 13:19:00 ZTomáš Hercegtomas.herceg@riganti.czDotVVM Tip 07: Use ComboBox to select values from a collection<blockquote><p><em>DotVVM Tips is a series of short articles showing interesting features of DotVVM. To learn more, visit our </em><a href="https://www.dotvvm.com/docs/4.0/pages/introduction"><em>Docs site</em></a><em>.</em></p><p><em><br></em></p></blockquote><p>Building forms is really easy! Bind a collection of objects in the <a href="https://www.dotvvm.com/docs/4.0/controls/builtin/ComboBox"><strong>ComboBox control</strong></a> and let the user select the value.</p><pre class="brush:dothtml"><code class="language-DOTHTML">// DotVVM view &lt;dot:ComboBox DataSource="{value: Customers}" ItemTextBinding="{value: FullName}" ItemValueBinding="{value: Id}" SelectedValue="{value: SelectedCustomerId}" /&gt;</code></pre><pre class="brush:csharp"><code class="language-CSHARP">// DotVVM viewmodel public List&lt;Customer&gt; Customers { get; set; } = new(); public int SelectedCustomerId { get; set; }</code></pre><p>The <strong>ItemTextBinding</strong> tells the control what property should serve as the display text on the list items. The <strong>ItemValueBinding</strong> tells that the Id property of the selected object will be stored to the property set as the <strong>SelectedValue</strong>.</p>Mon, 28 Nov 2022 13:18:00 ZTomáš Hercegtomas.herceg@riganti.czDotVVM Tip 06: Validation controls<blockquote><p><em>DotVVM Tips is a series of short articles showing interesting features of DotVVM. To learn more, visit our </em><a href="https://www.dotvvm.com/docs/4.0/pages/introduction"><em>Docs site</em></a><em>.</em></p><p><em><br></em></p></blockquote><p>DotVVM syntax is different from Razor. The <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/validation/controls">Validator control</a> can display validation errors for a particular property. You can choose to <strong>apply CSS classes</strong> on any element based on validation state of some property.</p><pre class="brush:dothtml"><code class="language-DOTHTML">// Razor &lt;InputText @bind-Value="Address.PostalCode" /&gt; &lt;ValidationMessage For="@(() =&gt; Address.PostalCode)" /&gt; // DotVVM &lt;dot:TextBox Text="{value: PostalCode}" /&gt; &lt;dot:Validator Value="{value: PostalCode}" /&gt;</code></pre><p>The validation framework in DotVVM is very powerful. Read more in the <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/validation/overview">Validation overview</a> page.</p>Mon, 21 Nov 2022 13:15:00 ZTomáš Hercegtomas.herceg@riganti.czDotVVM Tip 05: Access parent context using _parent or _root<blockquote><p><em>DotVVM Tips is a series of short articles showing interesting features of DotVVM. To learn more, visit our </em><a href="https://www.dotvvm.com/docs/4.0/pages/introduction"><em>Docs site</em></a><em>.</em></p><p><em><br></em></p></blockquote><p>DotVVM syntax is different from Razor. Instead of passing lambda expressions, use <strong>_root</strong>, <strong>_parent</strong> or <strong>_this</strong> expressions to reference different scopes in your viewmodel.</p><pre class="brush:dothtml"><code class="language-DOTHTML">// Razor &lt;button type="button" @onclick="@(() =&gt; RemoveTopping(topping))"&gt; x &lt;/button&gt; // DotVVM &lt;dot:Button Click="{command: _root.RemoveTopping(_this)}"&gt; x &lt;/dot:Button&gt;</code></pre><p>If you need to jump two scopes up, you can use <strong>_parent2</strong>. You can also create a child scope by setting the <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/data-binding/binding-context"><strong>DataContext property</strong></a> to some expression. All bindings inside the blocks will be evaluated within the specified binding context.</p>Mon, 14 Nov 2022 13:11:00 ZTomáš Hercegtomas.herceg@riganti.czDotVVM Tip 04: Join CSS classes without complicated expressions<blockquote><p><em>DotVVM Tips is a series of short articles showing interesting features of DotVVM. To learn more, visit our </em><a href="https://www.dotvvm.com/docs/4.0/pages/introduction"><em>Docs site</em></a><em>.</em></p><p><em><br></em></p></blockquote><p>DotVVM syntax is different from Razor. Instead of <strong>concatenating fragments of CSS classes</strong>, just use <strong>class-*</strong> property and specify a boolean expression indicating whether the class shall be included or not.</p><pre class="brush:dothtml"><code class="language-DOTHTML">// Razor &lt;div class="order-total @(Pizzas.Count &gt; 0 ? "" : "hidden")"&gt; Total: ... &lt;/div&gt; // DotVVM &lt;div class="order-total" class-hidden="{value: Pizzas.Count &gt; 0}"&gt; Total: ... &lt;/div&gt;</code></pre><p>You can join as many classes as you want. They will be dynamically added or removed as the underlying properties in the viewmodel change.</p>Mon, 07 Nov 2022 13:09:00 ZTomáš Hercegtomas.herceg@riganti.czDotVVM Tip 03: Generate URLs using RouteLink<blockquote><p><em>DotVVM Tips is a series of short articles showing interesting features of DotVVM. To learn more, visit our </em><a href="https://www.dotvvm.com/docs/4.0/pages/introduction"><em>Docs site</em></a><em>.</em></p><p><em><br></em></p></blockquote><p>DotVVM syntax is different from Razor. Instead of concatenating parts of URLs, the <a href="https://www.dotvvm.com/docs/4.0/controls/builtin/RouteLink"><strong>RouteLink component</strong></a> can build links to other pages from the route name and its parameters. </p><pre class="brush:dothtml"><code class="language-DOTHTML">// Razor &lt;a href="myorders/@item.OrderId" class="btn btn-success"&gt; Track &amp;gt; &lt;/a&gt; // DotVVM &lt;dot:RouteLink RouteName="OrderDetails" Param-id="{value: OrderId}" class="btn btn-success"&gt; Track &amp;gt; &lt;/dot:RouteLink&gt;</code></pre><p>The routes are registered in <a href="https://www.dotvvm.com/docs/4.0/pages/concepts/routing/overview"><strong>DotvvmStartup.cs file</strong></a>, and you can supply any parameters using the <strong>Param-* </strong>properties.</p>Mon, 31 Oct 2022 13:06:00 Z