Skip to content

DotVVM 4.2

Compare
Choose a tag to compare
@exyi exyi released this 11 Nov 13:37
· 425 commits to main since this release
767804b

Potential breaking changes

DotVVM 4.2 should generally be source compatible with the previous 4.1 version. We did not preserve binary compatibility, so please make sure all other DotVVM packages are at 4.2 version (especially if you see a MissingMethodException or similar error). Please let us know if we forgot to update some of our packages. We also had to change the behavior in ways which could in theory break something

  • The type hashes (the client-side type metadata id) might be different. You might need to adjust if you have it hardcoded somewhere. (see #1614, should only affect generic types)
  • HtmlGenericControl and RadioButton now attempts to render both the server-side value and client-side binding regardless of RenderSettings.Mode. If you relied on the value not being pre-rendered on the server, please explicitly use _page.EvaluatingOnServer ? null : OriginalExpression. To render value server-side only, switch to a resource binding.
  • Using a non-DotvvmProperty defined on a markup control is now a compile-time error (in value bindings). It used to only fail client-side
  • DotVVM does not reload dothtml files when run in Production environment (when DotvvmConfiguration.Debug=false).
  • JsComponent is always initialized asynchronously (see #1684)
  • View Modules are always initialized after dotvvm is initialized
  • Operator precedence of && and || now behaves the same as in C#
  • Unopened closing tag used to close all currently open elements -- for example in <a></b> the b tag closed the a tag. This glitch caused all sorts of problems, so we changed it to not close any elements.
  • RadioButton.Checked is deprecated, please use CheckedValue and CheckedItem properties (the property never worked, but now it might fail compile-time if used incorrectly)

staticCommand validation

Server-side validation now works in staticCommand methods. The validation has to be enabled using [AllowStaticCommand(StaticCommandValidation.Manual)] or [AllowStaticCommand(StaticCommandValidation.Automatic)] attribute. In the Automatic case, all arguments will be recursively validated using DataAnnotations attributes and IValidatableObject.Validate will be called. In both cases, additional validation errors can be returned using the StaticCommandModelState class:

[AllowStaticCommand(StaticCommandValidation.Manual)]
public string MyMethod(MyModel model)
{
    var modelState = new StaticCommandModelState();
    modelState.AddArgumentError(() => model.Property, "Property is invalid");
    modelState.FailOnInvalidModelState();
}

Enabling the validation will make the staticCommand invocation clear all validation errors currently being shown client-side. Note that client-side validation isn't currently implemented, so all invocation will round-trip to the server.

See related forum post for more examples.

Custom primitive types

In order to better support typed IDs, DotVVM now has support for custom primitive types. These types are always represented as a string client-side, which makes them allowed in places only for primitive types - notably SelectedValue property on selector components, but also in route parameters, and html attributes.

The custom primitive type must have ToString() and static TryParse(string, out T) methods. It must also implement the IDotvvmPrimitiveType interface. It's only a marker interface, since ToString method present on all types, and TryParse method is static and we can't use interface static methods due to support for the old framework. The type can be a class, a struct or a record.

A simple typed ID type could look like the following:

public struct OrderId: IDotvvmPrimitiveType
{
    public int Value { get; }
    public OrderId(int value)
    {
        this.Value = value;
    }
    public override string ToString() => 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;
    }
}

WebForms adapters

We have made a new package which helps in the process of migrating Web Forms apps to the new .NET using DotVVM - DotVVM.Adapters.WebForms. It currently contains HybridRouteLink control and RedirectToRouteHybrid extension method.

  • The <webforms:HybridRouteLink> control has the same functionality as the RouteLink control, but it falls back to Web Forms routes when a requested DotVVM route doesn't exist (which means that the page hasn't been migrated yet).
  • The RedirectToRouteHybrid extension method of the IDotvvmRequestContext also falls back to the Web Forms route in case the DotVVM route doesn't exist.

The package also works on .NET Core, where these fallbacks are disabled (because of the absence of System.Web). This allows to keep using the API even after the migration is finished.

state, patchState, setState, updateState in JS View Modules

We have added the state property and the following methods to the global dotvvm object, viewmodel knockout observables and JS module context (DotvvmModuleContext)

  • state - returns the current value of the view model
  • setState(newValue) - replaces the current value
  • patchState(newValues) - replaces only the specified properties, for example patchState({ UserName: "new-user" })
  • updateState(currentValue => computeNewValue(currentValue)) - applies the specified function onto the current view model

See simplified view model API in JS Modules on forum.dotvvm.com for more details and some context.

Knockout deferred updates (experimental feature)

DotVVM uses knockout.js to immediately synchronize the view model into the HTML DOM. Knockout essentially does this by subscribing to an update event on each view model variable which is used in the binding. When any value inside the view model is changed, all bindings using the value immediately get a notification and update the bound DOM element.

This normally works well, but if the binding uses very many different variables it can get equally many notification if large part of the view model gets changed. For example, you can have a binding which enumerates an array by counting number of certain elements in an array ({value: MyArray.Count(a => a.IsSpecial)}). Each array element is a separate viewmodel value, so the binding with get up to MyArray.Length update notifications, each time evaluating the binding and updating the DOM element. This mean we have got O(n^2) worst case complexity for updating the view model, which can be really slow (in some cases).

Knockout has got an ko.options.deferUpdates = true option, which makes the updates asynchronous and deduplicates the notifications. Updating all array elements will then lead to just one DOM update (with a slight delay). It was always possible to enable this option, in 4.2 we made it easier to enable and fixed a few bugs in DotVVM which occurred when delayed updates were enabled. The feature is still experimental, since we didn't thoroughly test it with all our components. You can enable/disable it for certain pages, if you encounter issues.

config.ExperimentalFeatures.KnockoutDeferUpdates.Enable().ExcludeRoute("SomePageThatOtherwiseBreaks");

Metrics

We instrumented DotVVM with System.Diagnostics.Metrics. All implemented metrics are listed in the DotvvmMetrics.cs file. We'd like to point out some of the important or interesting metrics:

  • dotvvm_viewmodel_size_bytes histogram - Size of the returned viewmodel JSON in bytes.
    • Labels route=RouteName and request_type=Navigate/SpaNavigate/Command/StaticCommand
  • dotvvm_request_duration_seconds - Time it took to stringify the resulting JSON view model.
    • Labels route, request_type and dothtml_file
    • You can use ASP.NET Core statistics about request duration, this metric makes it possible to split the measurement by DotVVM route name.
  • dotvvm_viewmodel_serialization_seconds histogram - Time it took to serialize view model to JSON objects.
    • Labels route and request_type
  • dotvvm_control_lifecycle_seconds histogram - Time it took to process a request on the specific route
    • Labels route and lifecycle_type=PreInit/Init/Load/PreRender/PreRenderComplete
  • dotvvm_command_invocation_seconds - Time it took to invoke a specific command method. Compared to request_duration_seconds this only includes the time spent in the command method, and is labeled by the executed binding (command)
    • Labels command and result=Ok/Exception/UnhandledException
  • dotvvm_staticcommand_invocation_seconds - Similar to command_invocation_seconds, but for staticCommand invocations
    • Labels command and result=Ok/Exception/UnhandledException
  • dotvvm_viewmodel_validation_errors_total histogram - Number view model validation errors returned to the client.
    • Labels route and request_type
  • dotvvm_uploaded_file_bytes - Total size of user-uploaded files
  • dotvvm_returned_file_bytes - Total size of returned files. Measured when the file is returned, not when downloaded by the client

If you are using Server-side viewmodel cache you might be also interested in the related metrics in order to measure if/how is the cache helping:

  • dotvvm_viewmodel_cache_hit_total counter - number of cache hits
  • dotvvm_viewmodel_cache_miss_total counter - number of cache misses - on a cache miss, the request fails and client has to retry uploading the entire view model.
  • viewmodel_cache_loaded_bytes_total counter - number of bytes loaded from the cache, you may interpret it as an upper bound estimate on the bandwidth saved

Prometheus configuration

If you want to use the prometheus-net library to expose the metrics in prometheus format, we recommend calling the Prometheus.MeterAdapter.StartListening method as soon as possible (at the start of Startup/DotvvmStartup is a good place), and configuring the buckets for histograms.

MeterAdapter.StartListening(new MeterAdapterOptions {
    ResolveHistogramBuckets = instrument => {
        // 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;
    }
});

Other notable changes

  • API for registering JS Method translations has been significantly upgraded - the registered method or property can be specified using a Linq.Expression. See #1608 for more details.
  • It is now possible to configure AutoUIConfiguration using the services.Configure<AutoUIConfiguration>(c => ...) pattern. ( #1626)
  • Executing a binding in an unexpected data context should result in much clearer error message. Additionally, bindings now only collect the data contexts which are required for execution, improving performance ( #1512)
  • FileUpload has Capture property which instructs browser to use a camera or a microphone (see HTML attribute: capture on MDN, PR #1621)
  • DotVVM now does not reload dothtml files in Production environment (#1542)
  • DotVVM can be initialized using the WebApplicationBuilder, without the need for Asp.Net Core Startup class (#1559)
  • Type hash used in client-side type metadata is computed differently for generic types. It should now exclude the assembly version and thus be more stable. (#1614)
  • Feature flags have IncludeRoute and ExcludeRoute methods. These methods allow incrementally adding or removing routes from the feature flag, enabling/disabling the feature for specific pages. (#1646)
  • AutoUI GetPropertiesToDisplay method is now public, making it easier to write custom automatic forms, views or grids (#1640)
  • Uploaded file storage is more resilient to temp directory deletion. It also better informs the user about missing write permissions or similar problems (#1655)
  • dothtml compilation warnings are logger using Asp.Net Core ILogger - the warnings appear on terminal after application startup (#1659)
  • When page fails and DotVVM error page is rendered, we also log the exception to ILogger (#1697)
  • Routing now supports optional parameters with type constraints (#1672)
  • JsComponent initialization and updates are run asynchronously. This speeds the the first knockout update, but more importantly avoids crashing the entire page when the component throws an exception. In such case, the exception is not caught by dotvvm nor knockout, making it easier to break the debugger on the exception ( #1684)
  • RadioButton now renders the client-side expression even in RenderSettings.Mode=Server (#1667)

All PRs: https://github.com/riganti/dotvvm/pulls?q=is%3Apr+milestone%3A%22Version+4.2%22+is%3Aclosed+sort%3Aupdated-asc