Only this pageAll pages
Powered by GitBook
1 of 80

WebMap for Blazor

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Modernization

WebMap Blazor

Modals and Dialogs

Research for Blazor

Conversion Tool

Deployment

It is important to note that Blazor Server operates as a stateful service, where all session data is maintained in the server's memory. In scenarios with multiple users, horizontal scaling may become necessary. Since Blazor utilizes persistent WebSocket connections, the scaling infrastructure must be capable of maintaining these connections and correctly routing messages to the appropriate instances that manage the user sessions. Additionally, the system must handle reconnection logic to ensure session continuity when disruptions occur.

Session Affinity

Session affinity, or "sticky sessions," is a crucial routing mechanism in Stateful server-side applications that ensures all requests from a specific user are consistently directed to the same server instance, particularly when scaling out.

Session affinity guarantees that all SignalR messages are routed to the same instance. This approach also automatically manages reconnections when necessary.

The following diagram outlines the services that offer WebSockets session affinity, along with the deployment options for scalable back-end infrastructure across the major cloud providers (AWS, Azure, GCP). Service selection should be driven by your specific technical requirements:

Overview

Migrate Legacy Desktop Windows Forms Applications to Web.

The modernization process converts a legacy Windows Forms application into a Web-based application, stored on a remote server and delivered over the Internet through a browser interface.

This has the following advantages:

  • No installation required.

  • Not limited to Windows OS.

  • The resulting App uses Cutting-edge web frameworks and patterns.

  • Can be distributed and billed using a SaaS model.

  • [more?]

Our Modernization process has the following goals:

  • Business Logic retention: Most legacy software is a 'black box' in terms of the inner workings of its business logic. Our modernization process does not require any major analysis, refactoring, or reimplementation of existing Business logic.

  • Maintainability: The output of the Modernization process is a set of software projects that can be considered software products on their own. Unlike many other transpilers, the resulting code is intended to be human readable and modifiable.

  • Extensibility: The modernized codebase is intended to be easily extended by seamlessly interfacing with third-party Web Component libraries and new business logic implementations.

  • [more?]

Interfacing with hardware devices

Modern web browsers have strict security restrictions preventing direct access to hardware. Even with physical access, browsers block such connections. In some cases, hardware may be in a remote facility, unable to connect directly to the user’s machine.

Hardware devices must connect to an Agent running on a local computer or embedded device, acting as a bridge to the Blazor app. The device can be remotely controlled and send real-time telemetry to the user. This follows standard IoT principles, enabling remote monitoring and control over a network.

Scaling Blazor apps with multiple hardware device connections across servers presents routing challenges, especially with many-to-one or one-to-one user-device mappings. Load balancers with sticky sessions ensure the session stays on the same server, while message routing mechanisms ensure control commands and telemetry reach the correct user-device pair.

Different cloud providers, such as AWS, Azure, and Google Cloud, offer various solutions for achieving scalable, reliable communication between users and remote hardware devices. These solutions often involve message brokers, event routing services, and IoT platforms:

Our solution

LiveView - Streaming Rendering patterns

To successfully convert a Windows Forms application to a Web application without needing to decouple the ViewModel or extract an API, we combine the LiveView and Streaming Rendering patterns.

  • LiveView Pattern: Enables real-time, interactive UIs by maintaining a persistent connection between client and server. The server handles rendering and logic, sending updates as the application's state changes, allowing dynamic content updates without full page reloads or extensive client-side JavaScript implementations.

  • Streaming Rendering: UI content is sent from the server to the client in small chunks as it’s generated, allowing users to view and interact with parts of the page immediately. This approach enables dynamic updates to specific portions of the page in response to user commands and asynchronously to internal parallel processes or external events.

By applying such patterns, we switch the Application architecture from this:

To this:

Our modernization process replaces the Windows Forms native OS rendering system with an HTML server-side renderer. The modernized architecture includes the following components:

  • Client Proxy: Relays raw HTML events to the server and updates the DOM based on the visual deltas sent by the server.

  • Full-Duplex Connection: Asynchronously streams commands and visual updates in near real-time.

  • Original Application Logic: HTML events are mapped to Windows Forms events and programmatically triggered in the original application logic. This initiates one or more business logic processes that modify the application state.

  • HTML Renderer: Directly bound to the application state, it reacts to its changes by generating new HTML code and computing the difference from the current visual state. Only this difference is sent back to the client.

What is Blazor?

Blazor is a powerful framework from Microsoft that enables the creation of interactive web applications using C# and .NET technologies, providing an alternative to JavaScript. It seamlessly integrates with existing .NET libraries and tools, offering a rich selection of visual controls to support modernization and extension of your legacy Windows Forms applications.

One of its standout features is the implementation of LiveView and streaming rendering patterns, making it an ideal choice for modernizing legacy applications.

The main advantages of using Blazor for modernization are:

  • Rich interactivity in C#: Handle arbitrary UI events and implement component logic entirely in C#, a language that is very likely to be familiar to your current development team.

  • Single Tech Stack: Your application will be implemented using a single tech stack, enabling you to build both the frontend and backend using a single codebase.

  • Efficient UI-Delta based Rendering: Blazor optimizes UI updates by carefully tracking changes in the DOM as components render, ensuring that updates are both rapid and efficient.

  • Ease of Debugging: The step-by-step debugging and Hot Reload features significantly enhance development efficiency.

Handling Reference Parameters in Async Methods with Ref<T>
Deployment options
From Windows Forms to Web
Cloud deployment options for Hardware devices Telemetry/control
Original Windows Forms App
LiveView and Streaming Rendering patterns applied
Blazor as a LiveView framework

Getting Started

Static Service Management

Handling Reference Parameters in Async Methods with Ref<T>

Situation:

In C#, reference parameters (ref and out) are not supported in asynchronous methods. This limitation can be problematic when you need to modify a value within an async method. The Ref<T> class and ReferenceExtensions class provide a solution to this issue by wrapping the value in a reference-like object that can be passed around and modified within async methods.

Approach: Ref Class

The Ref<T> class is a generic helper class designed to wrap a value and provide getter and setter methods for accessing and modifying the value. This class mimics the behavior of reference parameters by allowing the value to be modified indirectly.

Key Properties and Methods:

  • Getter: A Func<T> delegate that retrieves the value.

  • Setter: An Action<T> delegate that sets the value.

  • Value: The actual value being wrapped.

  • RefValueType: The type of the wrapped value.

  • Equals: Overrides the Equals method to compare the wrapped value.

  • HasMethod: Checks if the wrapped value has a specified method.

  • HasProperty: Checks if the wrapped value has a specified property.

  • HasField: Checks if the wrapped value has a specified field.

  • ToString: Overrides the ToString method to represent the wrapped value.

  • GetHashCode: Overrides the GetHashCode method to provide a hash code for the wrapped value.

  • Operators: multiple operators (logical, comparison and bool) are supported.

ReferenceExtensions Class

The ReferenceExtensions class provides an extension method to easily wrap a value in a Ref<T> instance. This extension method simplifies the creation of Ref<T> objects and allows for a more fluent syntax.

Key Method:

  • Ref: An extension method that wraps a value in a Ref<T> instance.

How the Approach Works

  1. Wrapping the Value: The Ref<T> class wraps the value and provides getter and setter methods to access and modify the value.

  2. Passing the Wrapper: The wrapped value (Ref<T>) is passed to the async method instead of the original value.

  3. Modifying the Value: Within the async method, the value can be modified using the Value property of the Ref<T> instance.

  4. Reflecting Changes: After the async method completes, the changes made to the Ref<T> instance are reflected in the original value.

Conclusion

The Ref<T> class and ReferenceExtensions class provide a robust solution for handling reference parameters in async methods. By wrapping the value in a Ref<T> instance, you can pass and modify the value within async methods, overcoming the limitations of the C# language.

How does Blazor Work?

Although any major UI design pattern, such as MVC or MVP, can be implemented in Blazor, it is especially well-suited for the MVVM pattern:

Blazor standard flow
  1. DOM event: The user triggers an event (e.g., 'onclick').

  2. Event relayed to View Model: Using a WebSocket connection, the event is sent to the server and then to the session View Model.

  3. State change: The View Model executes business logic and updates the model.

  4. View Model update: The View Model reacts to model changes, updating its state and triggering the HTML renderer.

  5. HTML rendering: The HTML renderer uses the bound parameters from the Model to generate new HTML and compute the difference with the current state.

  6. Visual delta transport: HTML differences are streamed asynchronously to the server, reducing data transfer and improving performance by updating only the necessary elements.

  7. DOM update: The view is refreshed by directly modifying the DOM with the received data.

Windows Forms in Blazor

To illustrate how we use Blazor to modernize Windows Forms applications, we will use this simple example:

Simple application to modernize

In this scenario, when the button is clicked, the model (Counter) is updated, and the UI automatically reflects the changes.

Upon the successful completion of our Modernization process, the expected outcome will be as follows (the code has been significantly simplified for clarity):

Modernized application structure
  • The original Windows Forms code remains essentially unchanged, with only minor modifications to accommodate the distinct characteristics of a desktop application to a server-side application.

  • Razor components, along with their associated bindings and observers, are automatically generated as part of the modernization process. There's no need for you to create or modify this code manually.

  • Bindings and observers trigger Razor components to generate HTML dynamically as needed. This HTML is then transmitted to the client, where the DOM is updated in real-time via Streaming Rendering, aiming to provide an experience as close as possible to that of a desktop application.

The following diagram details the execution flow triggered when the button is pressed:

Sample execution flow

Modernization Challenges

[Introduction: Write a short introduction for this.]

High Coupling

A typical legacy application is usually highly coupled. The design of Windows Forms components favors the proliferation of several antipatterns, such as business logic being directly written in button events, models being used as view models, and the lack of clear architectural layer separation.

Highly coupled Model, View Model and Controller in a WinForms App

Standard Approach for Modernizing Web Applications

Modernizing a legacy Windows Forms app to a standard Web application normally involves two expensive and time-consuming steps:

View Model decoupling, API extraction
  1. View Model decoupling: Modern web frameworks divide business logic (server) from presentation logic (client). From a Windows Forms perspective, this requires extensive analysis and the rewriting of most of the graphical logic.

  2. API Extraction: A high-level API must be designed to serve the client application. This requires a deep understanding of the business semantics and full access to a domain expert.

Solution and Project Structure

Guide to understand Solution Generator task

Create a native Blazor Component in a WebMap Window?

Chatiness

Debugging

Components Information

This is the list of components that currently Blazor tool support and his properties.

API Documentation

Create a new WebMap Component?

Errors and Troubleshooting

Project Structure

Change the default WebMap visual layout?

Create a native Blazor Window in a WebMap app?

Binaries size

Post Conversion

Create a new WebMap Window?

Footprint

License

How To?

Maintainability

Performance

WebMap: Angular vs Blazor

Extensibility

FAQ

Static Service Management

Technical explanation of service implementation.

Problem

One important challenge in the upgrade from Desktop to Web application is the handle of the static variables, in this scenario, the Desktop application only have one scope, if you execute multiple instances of the same application each one will have its own context, in Web application there are different scenarios.

For example, in Blazor WebAssembly deploy it will create one scope per tab or SignalR connection, so it won’t require any change given is a similar execution mode.

In Blazor Server we only have a server-client architecture so, It'll be the same server handling different connections, the scope of the static variables are global, so if you as a user open different tabs, they’ll share the same static variables and values, not per session/tab as expected. As we can see in the following diagram the life cycle of a static variable will persist during the whole server execution.

Static scope and service scope

Solution

After some research of different approaches a StaticService is implemented, this service is global and will store the value of the static variable per session, handling by itself the session and the storage. It will give us the following methods:

  • Get which will return the value stored in the service for this static variable.

  • Set will set the value into the service for this static variable.

  • InitializeData, when we have a static property auto-implemented in the source code with a default value, we need to keep the value, but C# doesn’t allow non-auto-implemented properties to be initialized, so this method will set the corresponding value to the property in the first call on Get.

This service will involve changes on the conversion of the static variables that we’ll review in the following section.

Conversion Changes

This feature implies that we need changes in the converted code, principally on the getters and setters, each one will require to call the methods. One important decision we made during the design of this feature is to keep the code visible instead of using some attribute in order to increase the visibility and the maintainability of the converted code. Here we have a basic example with no initialization:

Winforms:

Blazor:

There is another example when an initialization is set on the static variable.

Winforms:

Blazor:

Service Setup

The StaticService requires some configuration in the program of our new application, is necessary to add the Session, DistribuitedMemoryCache service, HttpContextAccessor and initialize the services as we can see in the following code:

These services are required to be provided to the StaticServices to handle the different sessions and identify which one is the current session that is being consulted.

Async/Await Feature for WebMap Blazor

Blazor Async/Await Service and Graph approach

In WebMap Blazor, we need the MessageBox Show and the InputBoxShow to be awaited. For this reason, it is necessary to have an asynchronous architecture for methods that call the MessageBox Show or the InputBox Show. For this documentation purposes we are calling every "awaitable" call, like that, an AwaitableCall.

But, what happens when a method that calls an AwaitableCall, is called inside another method? And then another method called those methods.

We can have a chain of async and await calls. But how can we determine in the migration where to put an await and where to put an async?

For this reason, we decided to build a Graph, to have all the dependencies between client code methods and calls. And, using a service, we are going to save in a Hash set all the async methods. By doing this, we can ask in the transformation rules if a method is registered as an async method.

A graph looking like this:

As you can see the MyMethod should be async. Why? Because if you follow the route of calls and dependencies, the last call is to an AwaitableCall.

The Blazor Async Method Service

This is the service that saves the dependencies graph(This graph is not saving or marking the async methods, just saving the call dependencies between methods), and then HashSet that has all the methods that should be async.

Where is the graph builded?. Well, let's take a look at the AsyncAwaitsCollector.

Here in the Collect method, the searcher will execute in every CSMethod node to build the dependencies.

Here we don’t need to register as method roots the methods coming from System or UpgradeHelpers.

Then when the Graph is built, we can now register all the async methods. Let’s jump to the BlazorAsyncAwaitsCollectorTask.

Here, the MarkAsyncMethods is called to go through the graph and register the methods that need to be async.

Supported Scenarios for MessageBox

Using this feature we are supporting the next scenarios in migration:

MessageBox call needs to wait a response to continue execution

This will be converted to

MessageBox does not need to wait for a response from the user. This is changed by the ShowNotification mechanism

This will be transformed to

Every method and call that has a MessageBox call and is not a simple notification, is going to have an async modifier.

For the MessageBox waiting a response

This will be transformed to

For a simple MessageBox

This will be transformed to

Not Supported Scenarios

If you have a scenario where an async method is being called in the FormLoad, the constructor or the DefInstance(from VBUC Migration), there will be a lot of issues. Please refer to the section

This scenario is not supported. The CreateInstance is async by the Async/Await feature

File Upload Functionality

Overview

The BlazorDCP solution implements file upload using a combination of Blazor UI components, a backend API controller, and dialog abstractions. The process is secure, session-aware, and integrates with the application's dialog infrastructure.

User Interface Layer

Component: WMOpenFileDialogFormComponent.razor

  • Purpose: Provides the UI for file selection and upload within a modal dialog, using Telerik's FileUpload ( ) component.

  • Key Elements:

    • <TelerikUpload>: Handles file selection and upload.

    • SaveUrl="/api/upload/save": Uploads files to the backend API endpoint.

    • OnUpload: Event handler to add session information to the upload request.

    • OnSuccess: Event handler to process the server response.

    • <TelerikButton>: Allows the user to cancel the dialog.

  • Session Handling: On upload, the component retrieves the current session ID from the HTTP context and adds it to the upload request as sessionId. This ensures the backend can associate the upload with the correct user session.

  • Dialog Integration: The component receives an OpenFileDialogForm model, which is used to manage dialog state and results. On successful upload, the file name is set on the dialog, the dialog result is set to OK, and the dialog is closed.

  • Extension: It extends the WMFormComponent in order to accomplish the dialog behavior.

Dialog Abstractions

Class: OpenFileDialog

  • Purpose: Represents the logic for displaying a file open dialog, similar to WinForms' OpenFileDialog.

  • Key Implementation: Uses a factory (DialogFormFactory) to create an instance of OpenFileDialogForm. The dialog is shown via ShowCore, which awaits the result of the form's ShowDialog() method.

Backend API Layer

Controller: UploadController

  • Purpose: Handles file upload requests from the Blazor UI.

  • Endpoint: POST /api/upload/save

  • Key Implementation: Accepts a file (IFormFile files) and a sessionId (from the form data). Validates that sessionId is a valid GUID using a strict regex. Saves the uploaded file to a session-specific folder under the web root, using a unique file name. Returns the file save location on success, or an error message and status code on failure.

  • Security: Only accepts session IDs that match the GUID format, preventing path traversal and other attacks.

End-to-End Flow

  1. User Action: The user opens a file dialog in the Blazor app (via OpenFileDialog and OpenFileDialogForm). Once the ShowDialog method is called, the execution thread gets blocked until the dialog gets closed, just like the behavior in WinForms.

  2. File Selection: The WMOpenFileDialogFormComponent displays the upload UI. The user selects a file.

  3. Upload Trigger: When the user uploads, the component:

    1. Retrieves the session ID from the HTTP context.

    2. Adds sessionId to the upload request.

    3. Sends the file to /api/upload/save.

  4. Backend Processing:

    1. UploadController.Save receives the file and session ID.

    2. Validates the session ID.

    3. Saves the file in a session-specific folder.

    4. Returns the file path or an error.

  5. Result Handling: On success, the Blazor component updates the dialog's state and closes it, making the uploaded file available to the application. The main executed thread blocked in step one is resumed.

Security and Session Awareness

  • Session Isolation: Each upload is tied to a session, and files are stored in folders named after the session's GUID.

  • Input Validation: The backend strictly validates the session ID to be a GUID, mitigating common file upload vulnerabilities.

Alternatives to Async Properties in C#

Current issue situation:

  • It is not allowed to have async properties in c#, it produces syntax errors, that’s why we need to look for alternatives.

  • If the property has code that could potentially be asynchronous because it has a call to, for example, a Gap.Blazor.MessageBox.Show which is an async method, the conversion tool generates an await inside the property code generating the compilation error.

Approach #1: Transform property to async methods (set and get).

This approach implies that any use of the "getter" requires an await so it can take the actual returned value instead of the task.

This approach could lead to poor legibility of the code, because of the massive amount of await sentences that could potentially be present in the code.

Example #1

Source code instance:

Migrated code instance:

The execution chain should add async and await keywords as required event for event handlers

The awaited task cannot be the entry point of the app because of some errors when it runs:

Result: It could lead to code hard to read and with slightly performance issues.

Approach #2: Changing source code, remove async modals as needed from methods that are consumed from properties

This could be the easiest solution, but could lead to problems when refactoring the customer code.

Button Component

Description

This component represents a button in Blazor using the Gap.Blazor.Button model. The button is rendered using the TelerikButton component and is dynamically styled based on the properties of the model.

Usage

Properties

  • buttonModel: Instance of the Gap.Blazor.Button model.

  • elementRef: Reference to the TelerikButton element.

Methods

  • onClickHandler(): Handles the button click event.

  • Focus(): Method to focus the button.

  • GetBackColorHex(): Gets the button's background color in hexadecimal format.

Events

  • OnClick: Event triggered when the button is clicked.

Styles

The button's style is dynamically defined using the GetComponentClass() method, and the background color is obtained through GetBackColorHex().

WebMap for Blazor Release Notes

This are release notes and versioning for WebMap for Blazor:

@using Gap.Blazor
@using Gap.Blazor.Components
@using Telerik.Blazor.Components
@inherits WMControlComponentBase
@namespace Gap.Blazor.Components

<WMStyleBase model=@buttonModel></WMStyleBase>
<div @onkeydown="@keyDownHandler">
    <TelerikButton Class="@(buttonModel.GetStringClasses()+" "+ buttonModel.GetToolTipClass())" Title="@buttonModel.ToolTipText" [email protected] [email protected] 
OnClick=@onClickHandler ThemeColor=@(ThemeConstants.Button.ThemeColor.Base) @ref="elementRef"
    TabIndex=@TabIndex>@buttonModel.Text</TelerikButton>
</div>

<style>
    [email protected](){
        background-color: @GetBackColorHex();
    }
</style>

Beta version

Release Notes

11-01-2024

ToolStripLabel

Description

This component represents a non-interactive label within a ToolStrip in Blazor using the Gap.Blazor.ToolStripLabel model. It is rendered inside a Telerik ToolBarTemplateItem and supports tooltip display and optional click interaction.

Usage

<WMStyleBase model=@toolStripLabel></WMStyleBase>

<ToolBarTemplateItem>
    <label class="@(toolStripLabel.GetStringClasses() + " " + toolStripLabel.GetToolTipClass())"
           title="@toolStripLabel.GetToolTipText()"
           onclick="@toolStripItemClick">
        @toolStripLabel.Text
    </label>
</ToolBarTemplateItem>

<style>
    [email protected]() {
        position: relative;
    }
</style>

Properties

  • toolStripLabel: Instance of the Gap.Blazor.ToolStripLabel model.

  • Text: The label text displayed in the toolbar.

  • ToolTipText: Tooltip shown when hovering over the label.

  • CSS Classes: Dynamically generated using GetStringClasses() and GetToolTipClass().

Methods

  • toolStripItemClick(MouseEventArgs): Invokes the PerformClick() method on the model when the label is clicked. This allows the label to optionally behave like a button if needed.

Dynamic Rendering

  • ToolBarTemplateItem: Used to render the label inside a Telerik ToolBar.

  • Label Element: Displays the text and handles optional click events.

  • Tooltip: Displayed via the title attribute.

Styles

  • Position

Events

  • Click: Triggered when the label is clicked, if PerformClick() is implemented in the model.

ProgressBar

Description

This component represents a progress bar in Blazor using the Gap.Blazor.ProgressBar model. It uses the Telerik ProgressBar component to visually indicate progress, supporting both determinate and indeterminate states based on the model’s value and range.

Usage

@if (this.model.Visible)
{
    <WMStyleBase model=@progressBar></WMStyleBase>
    <div class="@progressBar.GetStringClasses()">
        <TelerikProgressBar Indeterminate="this.Indeterminate"
                            Max="@this.Maximum"
                            Value="@this.Value">
            <ProgressBarLabel Visible="false"></ProgressBarLabel>
        </TelerikProgressBar>
    </div>
}

Properties

  • ProgressBar: Instance of the Gap.Blazor.ProgressBar model.

  • Maximum: The maximum value of the progress bar (default is 100).

  • Value: The current progress value (default is 0).

  • Indeterminate: Boolean indicating whether the progress bar is in an indeterminate state (true when Value == 0).

Methods

  • OnInitialized(): Subscribes to the model’s NotifyModelChanged event and initializes the progress bar state.

  • AdjustProgressBar(): Calculates the effective Maximum and Value based on the model’s Minimum, Maximum, and Value properties. Sets Indeterminate to true if Value == 0.

  • NotifyModelChanged(): Called when the model changes. Updates the progress bar state and triggers UI refresh.

Dynamic Rendering

  • TelerikProgressBar: Renders the progress bar UI.

  • Indeterminate Mode: Automatically enabled when the value is zero.

  • Label: Hidden by default using <ProgressBarLabel Visible="false" />.

Events

  • NotifyModelChanged: Triggers a recalculation of the progress bar’s state and updates the UI.

ListView

Description

This component represents a list view in Blazor using the Gap.Blazor.ListView model. It supports multiple view modes, including a detailed view rendered through a dedicated WMListViewDetails component. The component is designed to handle dynamic data and column configurations, with full support for selection and check state tracking.

Usage

@using System.Collections.ObjectModel
@using Gap.Blazor
@using Gap.Blazor.Components
@using System.Dynamic
@namespace Gap.Blazor.Components

@if (@listViewModel.View == ListViewMode.Details)
{
    <WMListViewDetails model="@listViewModel"></WMListViewDetails>
}

Properties

  • listViewModel: Instance of the Gap.Blazor.ListView model.

  • items: An observable collection of ExpandoObject used to represent dynamic data rows.

  • telerikGrid: Reference to a Telerik Grid component (used internally in detailed views).

  • DataAdapter: Instance of ListViewDataAdapter used to adapt the model’s data for rendering.

Methods

  • OnInitialized(): Initializes the data adapter and subscribes to model events:

    • Columns.ColumnHeaderAdded

    • Columns.ColumnHeaderRemoved

    • SelectedItems.AfterClear, AfterAdd

    • SelectedIndices.AfterClear, AfterAdd

    • CheckedIndices.AfterAdd, AfterRemove

  • OnParametersSetAsync(): Placeholder for handling parameter updates (currently no logic).

  • Columns(): Returns the ColumnHeaderCollection from the model.

Dynamic Rendering

The component conditionally renders the detailed view using:

  • WMListViewDetails: A specialized component for rendering the Details view mode.

  • View Mode Check: Only renders if listViewModel.View == ListViewMode.Details.

Events

  • ColumnHeaderAdded / Removed

  • SelectedItems / SelectedIndices / CheckedIndices: Add and clear events

  • NotifyModelChanged: Triggered indirectly via event subscriptions

ToolStripSeparator

Description

This component represents a visual separator within a ToolStrip in Blazor using the Gap.Blazor.ToolStripSeparator model. It is rendered using the Telerik ToolBarSeparator component and is used to visually divide groups of toolbar items.

Usage

<WMStyleBase model=@toolStripSeparator></WMStyleBase>
<ToolBarSeparator class="@toolStripSeparator.GetStringClasses()" />

Properties

  • toolStripSeparator: Instance of the Gap.Blazor.ToolStripSeparator model.

  • CSS Classes: Dynamically applied using GetStringClasses() to control appearance and spacing.

Dynamic Rendering

  • ToolBarSeparator: Renders a visual divider between toolbar items.

  • WMStyleBase: Applies model-based styling to the separator.

ToolTip

Description

This component represents a tooltip manager in Blazor using the Gap.Blazor.ToolTip model. It integrates with the Telerik Tooltip component to enable tooltip behavior for elements marked with the .webmap-tooltip CSS class. The tooltip is conditionally rendered based on the model’s Active property.

Usage

<WMStyleBase model=@toolTip></WMStyleBase>

@if (this.toolTip.Active)
{
    <TelerikTooltip TargetSelector=".webmap-tooltip" />
}

Properties

  • toolTip: Instance of the Gap.Blazor.ToolTip model.

  • Active: Boolean indicating whether the tooltip system is enabled.

Dynamic Rendering

  • TelerikTooltip: Only rendered when toolTip.Active is true.

  • TargetSelector: Applies tooltip behavior to all elements with the .webmap-tooltip class.

ToolStripStatusLabel

Description

This component represents a status label within a StatusStrip in Blazor using the Gap.Blazor.ToolStripStatusLabel model. It renders a styled label inside a Telerik ToolBarTemplateItem, supports tooltip display, click interaction, and dynamic layout behavior such as spring and auto size.

Usage

<WMStyleBase model=@toolStripStatusLabel></WMStyleBase>

<ToolBarTemplateItem class="spring-active">
    <label class="@(toolStripStatusLabel.GetStringClasses() + " " + toolStripStatusLabel.GetToolTipClass() + " BorderStyle")"
           title="@toolStripStatusLabel.ToolTipText"
           onclick="@toolStripItemClick">
        @toolStripStatusLabel.Text
    </label>
</ToolBarTemplateItem>

<WMToolStripStatusLabel3DBorderStyle ToolStripStatusLabel="@this.toolStripStatusLabel" />

Properties

  • toolStripStatusLabel: Instance of the Gap.Blazor.ToolStripStatusLabel model.

  • Text: The label text displayed in the status strip.

  • ToolTipText: Tooltip shown on hover.

  • Spring: If true, the label expands to fill available space.

  • AutoSize: If true, the label width is set to 100%; otherwise, it uses a fixed width.

  • TextAlign: Determines the horizontal alignment of the label content.

Methods

  • toolStripItemClick(MouseEventArgs): Invokes the PerformClick() method on the model when the label is clicked.

Dynamic Rendering

  • ToolBarTemplateItem: Used to render the label inside a Telerik ToolBar.

  • Label Styling: Combines model-based classes with tooltip and border styling.

  • WMToolStripStatusLabel3DBorderStyle: Renders additional 3D border effects based on the model.

Styles

[email protected]() {
    position: relative;
    text-align: [based on TextAlign];
    width: [100% if AutoSize, else fixed width];
}

[email protected]() .k-toolbar-item {
    width: [100% if Spring, else fit-content];
    display: contents;
}

Events

  • Click: Triggered when the label is clicked, invoking the model’s PerformClick() method.

var res = MessageBox.Show(...)
...

var res = DialogResult.None
res = MessageBox.Show(...)
...

if(MessageBox.Show(...) == DialogResult.Yes)
...

if(MessageBox.Show(...) != DialogResult.Yes)
var res = await MessageBox.Show(...)
...

var res = DialogResult.None
res = await MessageBox.Show(...)
...

if(await MessageBox.Show(...) == DialogResult.Yes)
...

if(await MessageBox.Show(...) != DialogResult.Yes)
...
MessageBox.Show("Error validating date")
...
...
Application.CurrentApplication.ShowSimpleNotification("Error validating date")
...
public void ShowMessage(string message)
{
	...
	var res = MessageBox.Show(...);
	...
}
public async void ShowMessage(string message)
{
	...
	var res = await Message.Show(...);
	...
}
public void ShowMessage(string message)
{
	...
	MessageBox.Show(...);
}
public void ShowMessage(string message)
{
	...
	Application.CurrentAplication.ShowSimpleNotification(...);
	...
}
Alternatives to Async Properties in C#
Graph Example

Finding

Description

Recommendation

Performance Overhead of Async Properties

Observed a slight performance overhead when using async properties due to the state machine generation.

Explore alternatives for performance-critical sections.

Complexity in Error Handling

Error handling in async properties can be complex, especially with multiple awaited operations.

Use helper methods or dedicated error handling strategies.

Testing Challenges

Unit testing async properties requires mocking and asynchronous testing patterns, adding complexity.

Develop clear testing strategies and use mocking frameworks effectively.

Alternative 1: Async Methods with Naming Convention

Using async methods (e.g., `GetAsyncValue()`) instead of async properties.

Adopt a consistent naming convention for async data retrieval.

Alternative 2: Lazy Initialization with Async Factory

Lazy initialization with an async factory method to populate the value.

Use `Lazy<Task<T>>` for delayed asynchronous initialization.

Alternative 3: Event-Based Approach

Using events to notify when the asynchronous operation completes and the value is available.

Implement event handlers and manage event subscriptions carefully.

https://www.telerik.com/blazor-ui/documentation/components/upload/overview

Label Component

Description

This component represents a label in Blazor using the Gap.Blazor.Label model. The label is styled dynamically based on the properties of the model and supports text alignment changes.

Usage

Properties

  • label: Instance of the Gap.Blazor.Label model.

Methods

  • OnInitialized(): Initializes the component and subscribes to the TextAlignChanged event.

  • SubscribeToModelChanges(): Method called when the TextAlignChanged event is triggered.

Dynamic Rendering

The component dynamically renders a label based on the Label model properties:

  • WMStyleBase: Applies styles based on the model.

  • label: Displays the text of the Label model.

Styles

The styles for the label are defined within the component:

  • display: Sets the display to flex.

  • overflow: Hides overflow content.

  • align-items: Aligns items based on the TextAlign property.

  • justify-content: Justifies content based on the TextAlign property.

  • border: Sets the border style based on the BorderStyle property.

Events

  • TextAlignChanged: Event triggered when the text alignment changes.

GroupBox Component

Description

This component represents a GroupBox in Blazor, which is used to group other components within a fieldset. The component is styled dynamically based on the properties of the GroupBox model.

Usage

Properties

  • groupBox: Instance of the Blazor.GroupBox model.

Methods

  • GetLegendForeColorHex(): Gets the legend's ARGB fore color in hexadecimal format.

Dynamic Rendering

The component dynamically renders a fieldset with a legend based on the GroupBox model properties:

  • WMStyleBase: Applies styles based on the groupBox model.

  • fieldset: Container for grouping child components.

  • legend: Displays the text of the GroupBox.

Styles

The styles for the fieldset and legend are defined within the component:

  • fieldset: Sets the width and border style.

  • legend: Positions the legend and sets its appearance.

Events

  • ChildContent: Renders the child content within the fieldset.

ListBox

Description

This component represents a list box in Blazor using the Gap.Blazor.ListBox model. It uses the Telerik ListBox component to display a list of selectable items, supporting both single and multiple selection modes, dynamic updates, and synchronization with the backend model.

Usage

Properties

  • ListBoxModel: Instance of the Gap.Blazor.ListBox model.

  • ListBoxRef: Reference to the Telerik ListBox component.

  • ListBoxData: Internal list of items (not directly used in rendering).

  • ListBoxSelectionModeValue: Stores the current selection mode.

Methods

  • OnInitialized(): Subscribes to model events such as item changes, selection changes, and mode changes.

  • SyncModelChanges(): Updates the UI based on model changes and rebinds the list box.

  • GetSelectionMode(): Maps the model’s SelectionMode to Telerik’s ListBoxSelectionMode:

    • SelectionMode.One → Single

    • Otherwise → Multiple

  • SelectedIndexChange(IEnumerable): Updates the model’s selected indices based on user interaction.

Dynamic Rendering

The component dynamically renders a list box based on the ListBox model:

WMStyleBase: Applies styles based on the model.

TelerikListBox: Displays the list of items with optional toolbar.

SelectionMode: Supports both single and multiple selection.

Events

  • Items Collection Events: AfterAdd, AfterAddRange, AfterClear, AfterInsert, AfterRemoveAt

  • Selection Events: SelectedIndexChanged, SelectedIndicesChanged, SelectionModeChanged

  • NotifyModelChanged: General event to trigger UI updates.

ImageListStreamer

Implementation of the ImageList control

Problem

Working on the implementation of the ImageList, we encounter the issue of management of resources through the ImageListStreamer. The ImageListStreamer is basically a serializer of all the images that are stored inside the ImageList component.

This ImageListStreamer is initialized by the .resx file associated with the .designer in which the ImageList is created. The .rex is the one that created the instance of the ImageListStreamer, serialized in base64 all the images and associated the data with a specific public token and the System.Windows.Forms.ImageListStreamer class.

This makes it very difficult to use this data to serialize out of the implementation of the WindowsForm component because it always needs the .dlls hashes to validate the information. This means that any custom implementation of the ImageListStreamer out of WindowsForm will fail when it tries to convert or deserialize the information storage in the .resx file.

Work around

As a way to interpret this mechanism through the resource, we manage to find a work-around that uses the same resource to get every image and storage in the ImageList array. Basically, the ImageList creates an instance of the ImageListStreamer and inside this, creates a method that takes the resource as a parameter and, having stored the keys for every image, gets the object Image associated with the key.

The problem with this solution is that it needs changes in the migrated code like the call of this method that fills the ImageCollection or adds every Image manually to the .resx file and associates that image name to the key so the resource can find it when it is searched.

Possible Solutions

A possible way to manage this issue is to create a pre-serializer that changes the values that reference the System.Windows.Form.ImageListStreamer for our implementation. That might let us create an instance that references the ImageStream in the .resx file and serialize normal inside the Blazor.ImageListStreamer.

Another solution is to somehow decode the images inside the ImageStream and storage separate in the .resx files of the migration project. This will help us with the work-around to minimize the manual changes in case the first solution does not work.

TabPage

Description

This component represents a single tab page within a TabControl in Blazor using the Gap.Blazor.TabPage model. It is rendered inside a Telerik TabStripTab and supports dynamic visibility, styling, and content layout. The component integrates with VB6 migration logic to support conditional rendering of tab pages.

Usage

Properties

  • tabPage: Instance of the Gap.Blazor.TabPage model.

  • Text: The title of the tab, displayed in the tab header.

  • ChildContent: The content rendered inside the tab page.

  • Parent: Reference to the parent TabControl.

Methods

  • OnInitialized(): Subscribes to the NotifyModelChanged event to trigger UI updates.

  • GetVisibility(): Determines whether the tab page should be rendered based on the parent TabControl's visibility logic. Supports VB6-style SetTabVisible behavior via VisibleAffected() and GetTabVisible().

Dynamic Rendering

  • TabStripTab: Renders the tab page within a Telerik TabStrip.

  • Visibility: Controlled by GetVisibility() to support conditional tab rendering.

  • CSS Styling: Applies padding and layout adjustments to the tab content area.

Styles

Events

  • NotifyModelChanged: Triggers UI updates when the model changes.

MessageBox

Description

This component represents a modal message box in Blazor using the Gap.Blazor.MessageBox model. It uses the Telerik Dialog component to display messages with configurable icons, text, and button layouts. The component supports various standard button configurations such as OK, OK/Cancel, Yes/No, and Yes/No/Cancel.

Usage

Properties

  • messageBox: Instance of the Gap.Blazor.MessageBox model.

  • LabelMessage.Text: The main message content displayed in the dialog.

  • Buttons: Enum (MessageBoxButtons) that determines which buttons are shown.

  • Icon: Enum (MessageBoxIcon) that determines which icon is displayed.

Methods

  • GetIcon(): Returns the appropriate icon image based on the MessageBoxIcon value:

    • Information → info icon

    • Warning → warning icon

    • Error → error icon

    • Question → question icon

    • Default → info icon

Dynamic Rendering

  • TelerikDialog: Renders the modal dialog.

  • DialogContent: Displays the icon and message text.

  • DialogButtons: Dynamically renders buttons based on the Buttons enum.

  • WMButtonComponent: Used to render each button, bound to the corresponding model.

Button Layouts

Enum Value
Buttons Rendered

Beta version

This Beta version of WebMap for Blazor includes the following release features:

Desktop Compatibility Platform (DCP)

The DCP support is focus on basic and complex controls requires for SKS demo controls like:

Mapping Framework

To generate mapping events and models for C# .Net control to Blazor control we use Telerik library framework to create custom control maps.

Features Implemented

  • Static variables management status: since in a web environment static variables represents a restriction on the sessions that can be created / accessed this reduce the capabilities of the tool, so we create a mechanism to map static variables as services assign.

  • .Net9 Framework outcome: our applications conversion results use references for .Net 9 Framework to use latest version of supported framework.

  • DataGridView support: initial challenges on conversion are Grids management so we support basic methods, properties and events required for DataGridView.

  • Mdi Forms conversion: emulate the winforms Mdi behavior of encapsulation of forms and allow navigation with several screens.

CheckBox

Description

This component represents a checkbox in Blazor using the Gap.Blazor.CheckBox model. It integrates with Telerik UI components and supports dynamic styling and state management based on the model's properties.

Usage

Properties

  • checkBox: Instance of the Gap.Blazor.CheckBox model.

Methods

  • OnInitialized(): Initializes the component and subscribes to the CheckedChanged and CheckStateChanged events.

Dynamic Rendering

The component dynamically renders a checkbox based on the CheckBox model properties:

  • WMStyleBase: Applies styles based on the model.

  • TelerikCheckBox: Represents the checkbox UI element.

  • Label: Displays the associated text.

Styles

The styles for the checkbox are defined within the component:

  • CSS Class: Dynamically generated using model.GetComponentClass().

  • Custom Styling: Can be extended via WMStyleBase and model-based class logic.

Events

  • CheckedChanged: Triggered when the checkbox value changes.

  • CheckStateChanged: Triggered when the check state changes (e.g., indeterminate).

@using Gap.Blazor.Components
@using Gap.Blazor.Extensions
@inherits WMControlComponentBase
@namespace Gap.Blazor.Components

@if (model.Visible)
{
    <WMStyleBase model=@model></WMStyleBase>
    <label class="@model.GetStringClasses() @GetAutoSizeClass()" tabindex=-1>@model.Text</label>

    <style>
        [email protected]() {
            display:flex;
            overflow: hidden;
            align-items : @this.label.TextAlign.GetAlignItemsStyle();
            justify-content : @this.label.TextAlign.GetJustifyContentStyle();
            border: @this.label.BorderStyle.ToCss();
        }
    </style>
}
@namespace Gap.Blazor.Components
@using Gap.Blazor
@using System.Collections
@using Telerik.Blazor.Components
@inherits WMControlComponentBase

@if (this.Visible)
{   
    <WMStyleBase model=@groupBox></WMStyleBase>
    <div>
        <fieldset class="@this.groupBox.GetStringClasses()">
            <legend>@this.Text</legend>
            @ChildContent
        </fieldset>
    </div>

    <style>
        fieldset {
            width: 100%;
            border: 1px LightGrey solid;
        }

        legend {
            position: absolute;
            float: none;
            transform: translateY(-50%);
            width: auto;
            margin: 0 0.3em 0 0.3em;
            padding: 0 0.3em;
            border-width: 0;
            font-size: inherit;
            background-color: inherit;
        }
        
        [email protected]() legend {
            color: @GetLegendForeColorHex() !important;
        }
    </style>
}
@using Gap.Blazor
@using Gap.Blazor.Components
@using Telerik.Blazor.Components
@inherits WMControlComponentBase
@namespace Gap.Blazor.Components

<WMStyleBase model=@ListBoxModel></WMStyleBase>
<TelerikListBox @ref="@ListBoxRef"
    Data="@ListBoxModel.ListControlItems"
    SelectionMode="@GetSelectionMode()"
    SelectedItemsChanged="SelectedIndexChangeCallback"
    Height="@ListBoxModel.Height.ToString()"
    Width="@ListBoxModel.Width.ToString()"
    class="@ListBoxModel.GetStringClasses()">
    <ListBoxToolBarSettings>
        <ListBoxToolBar Visible="true" />
    </ListBoxToolBarSettings>
</TelerikListBox>
<WMStyleBase model="this.model"></WMStyleBase>

<TabStripTab Title="@tabPage.Text" Visible="this.GetVisibility()">
    <div class="tabPage @this.tabPage.GetStringClasses()"
         @ref="elementRef"
         @onmouseup="MouseUpHandler"
         @onmousemove="MouseMoveHandler">
        @ChildContent
    </div>

    <style>
        .tabPage {
            top: 3px !important;
            bottom: 3px !important;
            left: 3px !important;
            right: 3px !important;
            width: auto !important;
            height: auto !important;
        }
    </style>
</TabStripTab>
.tabPage {
    top: 3px !important;
    bottom: 3px !important;
    left: 3px !important;
    right: 3px !important;
    width: auto !important;
    height: auto !important;
}
<TelerikDialog @bind-Visible="@this.messageBox.Visible"
               Title="@this.messageBox.Text"
               Width="@this.messageBox.GetWidthPx()"
               Height="@this.messageBox.GetHeightPx()">
    <DialogContent>
        <div style="display: flex; align-items: center; justify-content: center;">
            <img src="@this.GetIcon()" style="width:40px; height:40px; margin-right: 10px;" />
            <span style="white-space: pre-line;">@this.messageBox.LabelMessage.Text</span>
        </div>
    </DialogContent>
    <DialogButtons>
        @switch (this.messageBox.Buttons)
        {
            case MessageBoxButtons.OK:
                <WMButtonComponent model="@this.messageBox.ButtonOk" />
                break;
            case MessageBoxButtons.OKCancel:
                <WMButtonComponent model="@this.messageBox.ButtonOk" />
                <WMButtonComponent model="@this.messageBox.ButtonCancel" />
                break;
            case MessageBoxButtons.YesNo:
                <WMButtonComponent model="@this.messageBox.ButtonYes" />
                <WMButtonComponent model="@this.messageBox.ButtonNo" />
                break;
            case MessageBoxButtons.YesNoCancel:
                <WMButtonComponent model="@this.messageBox.ButtonYes" />
                <WMButtonComponent model="@this.messageBox.ButtonNo" />
                <WMButtonComponent model="@this.messageBox.ButtonCancel" />
                break;
        }
    </DialogButtons>
</TelerikDialog>

OK

OK

OKCancel

OK, Cancel

YesNo

Yes, No

YesNoCancel

Yes, No, Cancel


@using Gap.Blazor
@using Gap.Blazor.Components
@using Telerik.Blazor.Components
@inherits WMControlComponentBase
@namespace Gap.Blazor.Components

@if (model.Visible)
{
    <WMStyleBase model=@checkBox></WMStyleBase>
    <div class="@model.GetStringClasses()">
        <TelerikCheckBox Id="checkBoxControl" [email protected] @bind-Value="@checkBox.Checked"/>
        <label for="checkBoxControl">@this.Text</label>
    </div>
    <style>
        [email protected](){
        }
    </style>
}

Button

CheckBox

ComboBox

DataGridView

DateTimePicker

Form

GroupBox

Label

ListView

MdiContainers

MenuStrip

MessageBox

PictureBox

StatusStrip

TextBox

ToolStrip

ToolTip

Telerik UI for Blazor version 6.2.0

CheckedListBox

Description

This component represents a checked list box in Blazor using the Gap.Blazor.CheckedListBox model. It leverages the Telerik ListBox component to display a list of items with checkboxes, supporting dynamic selection, item checking, and model synchronization.

Usage

@using Gap.Blazor
@using Gap.Blazor.Components
@using Telerik.Blazor.Components
@inherits WMControlComponentBase
@namespace Gap.Blazor.Components

<WMStyleBase model=@CheckedListBoxModel></WMStyleBase>
<TelerikListBox @ref="@CheckedListBoxRef"
    Data="@CheckedListBoxModel.ListControlItems"
    SelectionMode="@GetSelectionMode()"
    SelectedItemsChanged="SelectedIndexChangeCallback"
    Height="@CheckedListBoxModel.Height.ToString()"
    Width="@CheckedListBoxModel.Width.ToString()"
    class="@CheckedListBoxModel.GetStringClasses()">
        <ListBoxToolBarSettings>
            <ListBoxToolBar Visible="true" />
        </ListBoxToolBarSettings>
        <ItemTemplate>
            @if (context != null)
            {
                var item = (ListControlItem)context;
                var checkboxValue = (bool?)item.Value;

                <TelerikCheckBox Id="@item.GetHashCode().ToString()" Value="@checkboxValue" ValueChanged="@( (bool? value) => OnCheckBoxValueChanged(value, item) )"/>
                <label for="@item.GetHashCode().ToString()">@item.Text</label>
            }
        </ItemTemplate>
</TelerikListBox>

Properties

  • CheckedListBoxModel: Instance of the Gap.Blazor.CheckedListBox model.

  • CheckedListBoxRef: Reference to the Telerik ListBox component.

Methods

  • OnInitialized(): Subscribes to various model events to keep the UI in sync with the backend.

  • SyncModelChanges(): Updates the UI based on model changes and rebinds the list box.

  • OnCheckBoxValueChanged(bool?, ListControlItem): Updates the check state of an item in the model.

  • GetSelectionMode(): Maps the model's SelectionMode to Telerik's ListBoxSelectionMode.

  • SelectedIndexChange(IEnumerable): Handles selection changes and updates the model accordingly.

Dynamic Rendering

The component dynamically renders a list of checkable items based on the CheckedListBox model:

  • WMStyleBase: Applies styles based on the model.

  • TelerikListBox: Displays the list of items.

  • TelerikCheckBox: Renders a checkbox for each item.

  • Label: Displays the text for each item.

Styles

  • Height/Width: Set dynamically from the model.

  • CSS Classes: Applied using CheckedListBoxModel.GetStringClasses().

Events

  • Items Collection Events: AfterAdd, AfterAddRange, AfterClear, AfterInsert, AfterRemoveAt

  • Selection Events: SelectedIndexChanged, SelectedIndicesChanged, SelectionModeChanged

  • ItemCheck: Triggered when an item’s check state changes.

  • NotifyModelChanged: General event to trigger UI updates.

StatusStrip

Description

This component represents a status strip in Blazor using the Gap.Blazor.StatusStrip model. It uses the Telerik ToolBar component to render a horizontal or vertical strip that displays status labels and optional tooltips. The component supports docking behavior and dynamic layout styling.

Usage

<WMStyleBase model=@statusStrip></WMStyleBase>

@if (statusStrip.ShowItemToolTips)
{
    <WMToolTipComponent [email protected]></WMToolTipComponent>
}

<TelerikToolBar [email protected]()>
    @foreach (var item in statusStrip.Items)
    {
        @if (item is ToolStripStatusLabel toolStripStatusLabel)
        {
            <WMToolStripStatusLabel model=@toolStripStatusLabel></WMToolStripStatusLabel>
        }
    }
</TelerikToolBar>

<style>
    [email protected]() {
        top: @styleTop;
        bottom: @styleBottom;
        left: @styleLeft;
        right: @styleRight;
        width: @styleWidth;
        height: @styleHeight;
    }
</style>

Properties

  • StatusStrip: Instance of the Gap.Blazor.StatusStrip model.

  • Items: A collection of ToolStripItem objects, typically ToolStripStatusLabel.

  • ToolTip: Tooltip model used when ShowItemToolTips is enabled.

  • Dock: Enum (DockStyle) that determines the position and layout of the status strip.

Methods

  • OnInitialized(): Initializes the component and calculates layout styles based on the Dock property.

  • GetStartStyles(): Sets the CSS positioning and dimensions based on the DockStyle:

    • Top, Bottom, Left, Right, or None

Dynamic Rendering

  • WMToolTipComponent: Conditionally rendered if ShowItemToolTips is true.

  • TelerikToolBar: Renders the strip and its items.

  • WMToolStripStatusLabel: Used to render individual status labels.

Styles

[email protected]() {
    top: [calculated];
    bottom: [calculated];
    left: [calculated];
    right: [calculated];
    width: [calculated];
    height: [calculated];
}
  • Styles are dynamically calculated based on the Dock property and dimensions from the model.

Events

  • No direct events in this component, but it reflects changes in the model such as tooltip visibility and item updates.

Application Data Component

Description

This component handles the rendering of various forms within the application using Blazor. It listens for changes in the application state and updates the UI accordingly.

Usage

@using Gap.Blazor
@using Gap.Blazor.Components
@using Microsoft.AspNetCore.Components.Rendering
@using System.Reflection
@namespace Gap.Blazor.Components

@code {
    private bool HasChanged = false;

    protected override void OnInitialized()
    {
        base.OnInitialized();
    }

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);
        if (firstRender)
        {
            Application.CurrentApplication.ItemHasChanged += CurrentApplication_ItemHasChanged;
        }
        this.HasChanged = false;
    }

    /// <summary>
    /// Handles the ItemHasChanged event of the CurrentApplication control.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
    private void CurrentApplication_ItemHasChanged(object sender, EventArgs e)
    {
        if (!HasChanged)
        {
            HasChanged = true;
            this.InvokeAsync(() => this.StateHasChanged());
        }
    }
}

<div>
    @foreach (Gap.Blazor.Form form in Application.ActiveForms)
    {
        if (!form.IsMdiChild){
            if (form.View != null)
            {
                var formArgs = new Dictionary<string, object>();
                formArgs.Add("model", form);
                <DynamicComponent @key="form" Type="@form.View" Parameters="@formArgs"></DynamicComponent>
            }
            else if (form is MessageBox msgBox)
            {
                <WMMessageBoxComponent @key="msgBox" model="msgBox"></WMMessageBoxComponent>
            }
            else if (form is InputBox inputBoxForm)
            {
                <WMInputBoxFormComponent @key="inputBoxForm" model="inputBoxForm"></WMInputBoxFormComponent>
            }
            else
            {
                <WMFormComponent @key="form" model="@form"></WMFormComponent>
            }
        }
    }
</div>

Properties

  • HasChanged: Boolean flag indicating if the application state has changed.

Methods

  • OnInitialized(): Initializes the component.

  • OnAfterRender(bool firstRender): Executes after the component has rendered. Subscribes to the ItemHasChanged event if it's the first render.

  • CurrentApplication_ItemHasChanged(object sender, EventArgs e): Handles the ItemHasChanged event of the CurrentApplication control.

Events

  • ItemHasChanged: Event triggered when an item in the application changes.

Dynamic Rendering

The component dynamically renders different types of forms based on their type:

  • DynamicComponent: Renders the form's view if it exists.

  • WMMessageBoxComponent: Renders a message box form.

  • WMInputBoxFormComponent: Renders an input box form.

  • WMFormComponent: Renders a general form.

RadioButton

Description

This component represents a radio button in Blazor using the Gap.Blazor.RadioButton model. It renders a native HTML <input type="radio"> element with dynamic styling and alignment, and supports model-driven state and event handling.

Usage

<WMStyleBase model=@radioModel></WMStyleBase>
<div class="@radioModel.GetStringClasses()">
    <label for="@radioModel.Name" style="@GetCheckAlignStyle()">
        <input type="radio" id="@radioModel.Name"
               checked="@radioModel.Checked"
               @onchange="onOptionChanged" />
        @radioModel.Text
    </label>
</div>

<style>
    [email protected]() {
        /* Additional styling can be applied here */
    }
</style>

Properties

  • RadioModel: Instance of the Gap.Blazor.RadioButton model.

  • Checked: Boolean indicating whether the radio button is selected.

  • Text: The label text displayed next to the radio button.

  • Name: Used as the id and for attributes for accessibility and grouping.

  • CheckAlign: Enum (ContentAlignment) that determines the alignment of the radio button relative to the text.

Methods

  • OnInitialized(): Subscribes to the CheckedChanged event to trigger UI updates.

  • onOptionChanged(ChangeEventArgs): Toggles the Checked state when the radio button is clicked.

  • GetCheckAlignStyle(): Returns a CSS style string based on the CheckAlign value to control layout direction and alignment.

Dynamic Rendering

  • HTML Input: Uses a native <input type="radio"> element for compatibility and accessibility.

  • Label Styling: Adjusted dynamically based on alignment preferences.

  • CSS Classes: Applied using radioModel.GetStringClasses() and model.GetComponentClass().

Alignment Options

CheckAlign Value

Layout Style

MiddleLeft, TopLeft, BottomLeft

Inline-flex, left-aligned radio button

MiddleRight, TopRight, BottomRight

Inline-flex, right-aligned radio button

TopCenter

Column-reverse (text below radio button)

BottomCenter

Column (text above radio button)

Events

  • CheckedChanged: Triggered when the radio button’s checked state changes.

PictureBox

Description

This component represents an image display control in Blazor using the Gap.Blazor.PictureBox model. It renders an image with optional tooltip text and click interaction, and visually reflects the enabled state through opacity.

Usage

@namespace Gap.Blazor.Components
@inherits WMControlComponentBase

<WMStyleBase model=@pictureBox></WMStyleBase>
<div class="@(model.GetStringClasses() + " " + pictureBox.GetToolTipClass())">
    @if (pictureBox.Image != null)
    {
        <img Title="@pictureBox.ToolTipText"
             alt="@pictureBox.Name"
             src="@this.GetImage()"
             onclick="@this.onClickHandler" />
    }
</div>

<style>
    [email protected]() {
        opacity: @this.GetOpacity();
    }
</style>

Properties

  • PictureBox: Instance of the Gap.Blazor.PictureBox model.

  • Image: The image to display, retrieved via pictureBox.Image.GetImage().

  • ToolTipText: Text shown when hovering over the image.

  • Name: Used as the alt attribute for accessibility.

Methods

  • GetOpacity(): Returns "1" if the component is enabled, "0.3" if disabled, simulating the enabled/disabled visual state.

  • onClickHandler(): Invokes the Click event on the model if the component is enabled.

  • GetImage(): Retrieves the image source from the model.

Dynamic Rendering

  • Conditional Image Display: The <img> tag is only rendered if pictureBox.Image is not null.

  • Tooltip and Alt Text: Provided via ToolTipText and Name.

  • CSS Styling: Dynamically adjusts opacity based on the enabled state.

Styles

[email protected]() {
    opacity: [1 or 0.3 depending on Enabled];
}

Events

  • Click: Triggered when the image is clicked, if the component is enabled.

Solution Generator

This document helps to understand the SolutionGeneration task, starting from the ProjectGeneration to the WFNetConversionTool call and settings to execute the Generation service.

First thing to understand is what ProjectGeneration does. ProjectGeneration contains the services and templates that generate the output files for a project, using some configurations. In this file, we will be focusing on the SolutionGeneration for Blazor.

We need to start talking about the Interfaces of the Abstractions. This interfaces define the properties and methods needed to execute the generation of the project.

We can find the interfaces in the Mobilize.ProjectGeneration.Abstraction project under the SolutionGenerator repository.

Go to IProjectGenerator, we can find some properties. You can see this properties as parameters that the Generate task needs.

Pay special attention to this method definition, because this will be called inside the Task to call the generator you need.

Now that we see this Interface, we need to understand… Why is this needed? Well, let’s jump on it.

Under the same project, we have a class called ProjectGeneratorBase.

This class implements the IProjectGenerator interface. If you navigate a bit in this class, you will find the GenerateProject implementation.

Now, we need to use the template used by the respective GenerateProject. In this case, we should go into the SolutionTemplate.tt.

Avoid touching the SolutionTemplate.cs

You can modify the template as we did here.

And save the file. When you save, a popup is showing. Press Yes. This is to synchronize the tt with the cs(the cs is generated dynamically by handlebars).

Pack the projects as a nupkg

To test the changes we can pack the projects locally, or, upload the changes to a branch and wait for the build to generate an alpha version.

To pack locally just run the next command under the build folder. Change the path for every csproj

nuget.exe pack 
S:\Repositories\EF\EF-TransformationCore\src\Mobilize.ProjectGeneration.Abstractions\Mobilize.ProjectGeneration.Abstractions.nuspec -version 22.33.11 -properties configuration=debug

Test the changes

Once you pack the changes, you need to install the package in the WFNetConversionTool. If you pack it locally, add the build folder to the package sources in the Nuget Package Manager of Visual Studio.

Or install the alpha version if you commit the changes to a branch.

Changes in the WFNetConversionTool

Now, is necessary to add the BlazorSolutionGenerationTask and the BlazorSolutionGenerator

Here we have our BlazorSolutionGenerator, that will set the params and config to call the GenerateProject from the ProjectGeneration.

Here in the Run method, we need to pass the config to the SolutionGenerationParams

And now, we can call the BlazorSolutionGenerator from the Task.

WMListViewDetails

Description

This component renders the Details view of a Gap.Blazor.ListView using a virtualized Telerik Grid. It supports dynamic columns, checkbox selection, and real-time updates to the grid based on changes in the underlying model. The component is optimized for performance with virtual scrolling and dynamic data binding.

Usage

Properties

  • ListViewModel: Instance of the Gap.Blazor.ListView model.

  • telerikGrid: Reference to the Telerik Grid component.

  • DataAdapter: Adapter that transforms ListViewItem data into a format compatible with Telerik Grid.

Methods

  • OnInitialized(): Subscribes to model events for item and subitem changes.

  • OnDataRead(GridReadEventArgs): Handles virtual scrolling and paging by returning a DataSourceResult from the adapter.

  • selectionChangeHandler(): Maps selected grid row to the corresponding ListViewItem and triggers ListViewItemClickAction and SelectedIndexChanged.

  • Items_AfterAdd / AfterInsert / BeforeRemove / AfterClear: Synchronize grid data with model changes.

  • UpdateSubItem(): Updates a specific cell in the grid when a subitem changes.

  • RecallOnChange_Data(): Forces the grid to refresh its state.

  • GetItemCheckedValue(index): Returns the checked state of a ListViewItem.

  • OnItemCheckBoxValueChanged(value, index): Updates the checked state of a ListViewItem.

Dynamic Rendering

  • GridColumns: Dynamically generated based on listViewModel.Columns.

  • Checkbox Column: Conditionally rendered if CheckBoxes is enabled.

  • Virtual Scrolling: Enabled via ScrollMode="Virtual" and OnRead.

Styles

Events

  • Items Collection Events: AfterAdd, AfterInsert, BeforeRemove, AfterClear

  • SubItem Updates: UpdateSubItem

  • Selection Events: SelectedItemsChanged, ListViewItemClickAction, SelectedIndexChanged

Form

Description

This component represents a windowed form in Blazor using the Gap.Blazor.Form model. It uses the Telerik Window component to simulate a WinForms-style form, supporting features like modal behavior, draggable windows, control boxes, MDI containers, and dynamic positioning.

Usage

Properties

  • Form: Instance of the Gap.Blazor.Form model.

  • TelerikWindowRef: Reference to the Telerik Window component.

  • Draggable: Boolean indicating whether the form is draggable (disabled for MDI containers).

  • Start Position Properties: LeftStartPosition, TopStartPosition, StyleStartPosition, TransformStartPosition control the initial window placement.

Methods

  • OnInitialized(): Subscribes to model and component events, initializes file support, and calculates start position styles.

  • FormClosing(): Invokes the model’s Close() method.

  • GetWindowState(): Maps the model’s WindowState to Telerik’s WindowState.

  • ActivateView(): Focuses the window when activated.

  • Focus(): Focuses the first control using MainTabOrderService.

  • GetStartPositionStyles(): Calculates CSS positioning based on StartPosition.

  • GetIconImage(): Retrieves the form’s icon image.

  • GetHeightPX() / GetWidthPX(): Calculates the form’s dimensions including frame adjustments.

  • BorderStyleToCss(): Converts FormBorderStyle to a CSS-compatible border style.

Dynamic Rendering

The component dynamically renders a windowed form based on the Form model:

  • WMStyleBase: Applies styles based on the model.

  • TelerikWindow: Provides the window UI with title, actions, and content.

  • WindowTitle: Displays the form’s icon and title.

  • WindowActions: Renders minimize, maximize, and close buttons based on model flags.

  • WindowContent: Hosts child content and optionally an MDI container.

Styles

Events

  • Activated

  • AutoScrollChanged

  • ControlBoxChange

  • ActivateView

  • NotifyModelChanged

ComboBox

Description

This component represents a combo box in Blazor using the Gap.Blazor.ComboBox model. It utilizes the Telerik ComboBox component to provide a searchable, filterable dropdown list with support for custom values and dynamic styling.

Usage

Properties

  • ComboBox: Instance of the Gap.Blazor.ComboBox model.

  • ElementRef: Reference to the Telerik ComboBox component.

  • Items: A dynamically generated list of objects with text and value fields based on ListControlItems.

  • Value: Bound to the selected value in the combo box.

Methods

  • OnInitialized(): Subscribes to model events such as SelectedIndexChanged, SelectedValueChanged, ValueMemberChanged, DataSourceChanged, DisplayMemberChanged, DropDownStyleChanged, and Items.AfterAdd.

  • Focus(): Focuses the combo box using the elementRef.

Dynamic Rendering

The component dynamically renders a combo box based on the ComboBox model:

  • WMStyleBase: Applies styles based on the model.

  • TelerikComboBox: Provides the dropdown UI with filtering and custom value support.

  • NoDataTemplate: Displays a message when no items are available.

Styles

  • Dropdown Positioning: Adjusted using .k-animation-container.

  • Height and Font Size: Dynamically set based on the model.

  • List Item Styling: Font size is derived from the model’s font settings.

Events

  • SelectedIndexChanged

  • SelectedValueChanged

  • ValueMemberChanged

  • DataSourceChanged

  • DisplayMemberChanged

  • DropDownStyleChanged

  • Items.AfterAdd

MonthCalendar

Description

This component represents a calendar control in Blazor using the Gap.Blazor.MonthCalendar model. It uses the Telerik Calendar component to support single or range date selection, with customizable appearance, date constraints, and interactive behaviors such as focus handling and cell rendering.

Usage

Properties

  • MonthCalendar: Instance of the Gap.Blazor.MonthCalendar model.

  • SelectedDate: Bound to SelectionStart in single selection mode.

  • RangeStart / RangeEnd: Used in range selection mode.

  • CurrentDate: Controls the visible month in the calendar.

  • CalendarRef: Reference to the Telerik Calendar component.

  • CalendarWrapperRef: Reference to the calendar container for JS interop.

Methods

  • OnInitialized(): Subscribes to model events and initializes selection.

  • OnAfterRenderAsync(): Registers JS focus handler on first render.

  • GetSelectionMode(): Returns Single or Range based on MaxSelectionCount.

  • OnCellRenderHandler(): Adds CSS classes to cells outside min/max range or exceeding selection bounds.

  • RangeStartChangeHandler() / RangeEndChangeHandler(): Updates selection range and triggers events.

  • SetSelectionRange(start, end): Applies selection logic with max range enforcement.

  • SyncModelSelectionChanges(): Updates selection and triggers UI refresh.

  • UpdateSelection(): Refreshes calendar view and updates internal state.

  • OnDateChanged() / OnDateSelected(): Triggers model events for date changes.

Dynamic Rendering

  • TelerikCalendar: Renders the calendar UI with support for:

    • Single or range selection

    • Min/Max date constraints

    • Week numbers

    • Custom cell rendering

  • CSS Classes: Dynamically applied to hide today button or circle.

Styles

Events

  • MaxDateChanged / MinDateChanged / DateChanged

  • MaxSelectionCountChanged / ShowWeekNumbersChanged

  • ShowTodayChanged / ShowTodayCircleChanged

  • OnDateChanged / OnDateSelected

MenuStrip

Description

This component represents a menu strip in Blazor using the Gap.Blazor.MenuStrip model. It renders a hierarchical menu using the Telerik Menu component and dynamically builds the menu structure from a ToolStripItemCollection. It supports nested submenus and click event handling for each item.

Usage

Properties

  • menuStrip: Instance of the Gap.Blazor.MenuStrip model.

  • menuItems: A list of MenuItem objects representing the menu hierarchy.

MenuItem Class

Methods

  • OnInitialized(): Subscribes to the Click event and builds the menu structure from the model.

  • buildMenuList(ToolStripItemCollection): Recursively constructs the top-level menu items.

  • buildMenuItem(ToolStripItem): Converts a ToolStripItem into a MenuItem, including subitems if present.

  • buildSubItems(ToolStripItemCollection): Recursively builds nested submenus.

  • OnClickHandler(MenuItem): Executes the PerformClick() method on the associated ToolStripItem.

  • GetBackColorHex(): Returns the background color in hex format, defaulting to #FDFDFDFF if not set.

Dynamic Rendering

  • TelerikMenu: Renders the menu UI with support for nested items.

  • Dynamic Styling: Background and text colors are derived from the model.

  • Submenus: Automatically rendered based on the SubItems property.

Styles

Events

  • Click: Triggered when a menu item is clicked, invoking the associated ToolStripItem.PerformClick().

ToolStrip

Description

This component represents a horizontal or vertical toolbar in Blazor using the Gap.Blazor.ToolStrip model. It uses the Telerik ToolBar component to render a collection of interactive items such as buttons, labels, and separators. The component supports tooltips, autosizing, and dynamic item rendering.

Usage

Properties

  • toolStrip: Instance of the Gap.Blazor.ToolStrip model.

  • Items: A collection of ToolStripItem objects, including:

    • ToolStripLabel

    • ToolStripButton

    • ToolStripSeparator

  • ShowItemToolTips: Boolean indicating whether tooltips should be displayed.

  • ToolTip: Tooltip model used when ShowItemToolTips is enabled.

Methods

  • GetAutoSizeClass(): Returns "AutoSize" if the toolbar should automatically adjust its height.

Dynamic Rendering

  • TelerikToolBar: Renders the toolbar container.

  • WMToolStripLabel / Button / Separator: Rendered dynamically based on the item type.

  • WMToolTipComponent: Conditionally rendered if tooltips are enabled.

Styles

Events

  • No direct events in this component, but individual items (e.g., buttons) may trigger their own events.

TabControl

Description

This component represents a tabbed interface in Blazor using the Gap.Blazor.TabControl model. It uses the Telerik TabStrip component to render tab pages with customizable alignment, appearance, size mode, and visibility logic. It supports dynamic tab switching and layout updates based on model changes.

Usage

Properties

  • TabControl: Instance of the Gap.Blazor.TabControl model.

  • ChildContent: The tab pages to be rendered inside the tab strip.

  • Alignment: Determines the position of the tabs (Top, Bottom, Left, Right).

  • Appearance: Controls the visual style of the tabs (Normal, Buttons, FlatButtons).

  • SizeMode: Controls how tab sizes are calculated (Fixed, FillToRight).

  • Multiline: If false, enables horizontal scrolling for tabs.

Methods

  • OnInitialized(): Subscribes to model events for alignment, appearance, size, multiline, and tab collection changes.

  • GetAppearanceStringClass(): Returns a CSS class based on the Appearance setting.

  • GetItemSizeHeight() / GetItemSizeWith(): Calculates tab dimensions based on alignment and size mode.

  • GetTabAlignmentFromSizeMode(): Maps SizeMode to Telerik’s TabStripTabAlignment.

  • TabChangedHandler(int): Updates the selected tab index, accounting for hidden tabs.

  • GetActiveTabIndex(): Returns the index of the currently selected tab, adjusted for visibility.

Dynamic Rendering

  • TelerikTabStrip: Renders the tab interface with dynamic alignment, appearance, and scroll behavior.

  • CSS Styling: Font and size styles are applied per tab item based on the model.

  • Visibility Logic: Adjusts tab index to account for hidden tabs using VisibleAffected() and GetPageIndex().

Styles

Events

  • AlignmentChanged

  • AppearanceChanged

  • ItemSizeChanged

  • MultilineChanged

  • SizeModeChanged

  • SelectedIndexChanged

  • Controls Collection Events: AfterAdd, AfterRemove, AfterClear, AfterInsert

@inherits WMListView
@using Gap.Blazor.Components
@using System.Dynamic
@using Telerik.Blazor.Components
@using Telerik.DataSource
@using Telerik.DataSource.Extensions
@namespace Gap.Blazor.Components

<WMStyleBase model=@listViewModel></WMStyleBase>
<div class="@listViewModel.GetStringClasses()">
    <TelerikGrid Height="100%" Width="100%" @ref="telerikGrid"
                 SelectionMode="GridSelectionMode.Single"
                 OnRead="@OnDataRead"
                 SelectedItemsChanged="@((IEnumerable<ExpandoObject> items) => selectionChangeHandler())"
                 RowHeight="20" PageSize="50"
                 ScrollMode="@GridScrollMode.Virtual"
                 Resizable="true">
        <GridColumns>
            @if (listViewModel.Columns.Count > 0)
            {
                if (listViewModel.CheckBoxes)
                {
                    <GridColumn FieldType="typeof(bool)" Width="16px">
                        <Template>
                            @{
                                var index = this.DataAdapter.GetRowIndex((ExpandoObject)context);
                                <div style="display: flex; justify-content: center;">
                                    <TelerikCheckBox Value=@GetItemCheckedValue(index)
                                                     ValueChanged="@((bool value) => OnItemCheckBoxValueChanged(value, index))"/>
                                </div>
                            }
                        </Template>
                    </GridColumn>
                }
                foreach (var column in listViewModel.Columns.Cast<ColumnHeader>())
                {
                    <GridColumn [email protected] FieldType="typeof(string)" Width="@($"{column.Width}px")">
                        <HeaderTemplate>
                            <span>@column.Text</span>
                        </HeaderTemplate>
                    </GridColumn>
                }
            }
        </GridColumns>
    </TelerikGrid>
</div>
[email protected]() .k-table,
[email protected]() .k-grid {
    font-size: inherit;
}

[email protected]() tr > td,
[email protected]() tr > th {
    padding-block: 0px !important;
    padding-inline: 0px !important;
    overflow: hidden !important;
    text-overflow: ellipsis !important;
    white-space: nowrap !important;
}
<WMStyleBase [email protected]></WMStyleBase>

<TelerikWindow class="@model.GetStringClasses()"
               Modal="@this.Form.Modal"
               [email protected]
               State="@GetWindowState()"
               ContainmentSelector="@SetContainmentSelector()"
               Draggable="@Draggable"
               @ref="this.TelerikWindowRef">
    <WindowTitle>
        @if (this.Form.Icon != null)
        {
            <img src="@this.GetIconImage()" width="18em">
        }
        @Text
    </WindowTitle>
    <WindowActions>
        @if (this.Form.ControlBox)
        {
            @if (this.Form.MinimizeBox)
            {
                <WindowAction Name="Minimize"></WindowAction>
            }
            @if (this.Form.MaximizeBox)
            {
                <WindowAction Name="Maximize"></WindowAction>
            }
            <WindowAction Name="Close" OnClick="@FormClosing"></WindowAction>
        }
    </WindowActions>
    <WindowContent>
        <CascadingValue Value=MainTabOrderService>
            @ChildContent
            @{
                if (Form.IsMdiContainer)
                {
                    <WMMdiContainerComponent MdiChildren="@Form.MdiChildren"></WMMdiContainerComponent>
                }
            }
        </CascadingValue>
    </WindowContent>
</TelerikWindow>
[email protected]() {
    border: [based on FormBorderStyle];
    position: [absolute or centered];
    left: [calculated];
    top: [calculated];
    transform: [calculated];
    height: [Form.Height + 45px];
    width: [Form.Width + 2px];
}

[email protected]() .k-window-content,
[email protected]() .k-prompt-container {
    overflow: [based on AutoScroll];
}
@namespace Gap.Blazor.Components
@using Gap.Blazor
@using System.Collections
@using Telerik.Blazor.Components
@inherits WMControlComponentBase

@if (this.comboBox.Visible)
{
    <WMStyleBase model=@comboBox></WMStyleBase>
    <div class="@comboBox.GetStringClasses()" @onkeydown="@keyDownHandler">
        <TelerikRootComponent>
            <TelerikComboBox TValue="string"
                             TItem="object"
                             Data="@this.items"
                             Enabled="@this.Enabled"
                             AllowCustom="true"
                             Filterable="true"
                             ValueField="text"
                             TextField="value"
                             Width="100%"                                
                             @bind-Value="@this.Value" @ref="elementRef" TabIndex=@TabIndex>
                <NoDataTemplate>
                    <div>
                        <TelerikSvgIcon Icon="@SvgIcon.InfoCircle" Size="@ThemeConstants.SvgIcon.Size.Large" />
                        <br />
                        <br />
                        No items to display...
                    </div>
                </NoDataTemplate>
            </TelerikComboBox>
        </TelerikRootComponent>
    </div>

    <style>
        [email protected]() .k-animation-container {
            top: @(comboBox.Height + "px") !important;
            left: 0px !important;
        }
        [email protected]() .k-combobox {
            height: @(comboBox.Height + "px");
            font-size: inherit;
        }
        [email protected]() .k-list-item {
            font-size: @comboBox.Font.GetFontSize();
        }
    </style>
}
@if (this.Visible)
{
    <WMStyleBase model="@this.MonthCalendar"></WMStyleBase>

    <div class="@(this.MonthCalendar.GetStringClasses() + this.GetShowTodayStyle() + this.GetShowTodayCircleStyle())"
         tabindex="-1"
         @ref="this.CalendarWrapperRef">
        <TelerikCalendar AllowReverse="true"
                         Date="@this.CurrentDate"
                         Max="@this.MonthCalendar.MaxDate"
                         Min="@this.MonthCalendar.MinDate"
                         OnCellRender="@this.OnCellRenderHandler"
                         RangeEnd="@this.RangeEnd"
                         RangeEndChanged="@this.RangeEndChangeHandler"
                         RangeStart="@this.RangeStart"
                         RangeStartChanged="@this.RangeStartChangeHandler"
                         SelectionMode="@this.GetSelectionMode()"
                         ShowWeekNumbers="@this.MonthCalendar.ShowWeekNumbers"
                         Size="@ThemeConstants.Calendar.Size.Small"
                         View="CalendarView.Month"
                         @bind-Value="@this.SelectedDate"
                         @ref="this.CalendarRef">
        </TelerikCalendar>
    </div>
}
.hide-today-button { /* hides today button */ }
.hide-today-circle { /* hides today circle */ }
.hide-date { visibility: hidden; }
.exceeds-max-range { opacity: 0.5; pointer-events: none; }
@inherits WMControlComponentBase
@namespace Gap.Blazor.Components

<WMStyleBase [email protected]></WMStyleBase>
<TelerikMenu [email protected]()
             [email protected]
             ItemsField="SubItems"
             TextField="Text"
             OnClick="@((MenuItem item) => OnClickHandler(item))">
</TelerikMenu>

<style>
    [email protected]() {
        background-color: @GetBackColorHex() !important;
        color: @menuStrip.GetForeColorHex() !important;
    }     
    .@(model.GetComponentClass()+".k-menu") {
        height: fit-content;
        width: 100%;
        z-index: 1;
    }
    .@(model.GetComponentClass()+".k-menu") li {
        color: inherit !important;
    }
</style>
public class MenuItem
{
    public string Text { get; set; }
    public List<MenuItem>? SubItems { get; set; }
    public ToolStripItem ToolStripItem { get; set; }

    public MenuItem()
    {
        Text = string.Empty;
        ToolStripItem = new ToolStripItem();
    }
}
[email protected]() {
    background-color: [model.BackColor] !important;
    color: [model.ForeColor] !important;
}

[email protected]().k-menu {
    height: fit-content;
    width: 100%;
    z-index: 1;
}

[email protected]().k-menu li {
    color: inherit !important;
}
<WMStyleBase model=@toolStrip></WMStyleBase>

@if (toolStrip.ShowItemToolTips)
{
    <WMToolTipComponent [email protected]></WMToolTipComponent>
}

<TelerikToolBar Class=@($"{this.toolStrip.GetStringClasses()} {GetAutoSizeClass()}")>
    @foreach (var item in toolStrip.Items)
    {
        @if (item is ToolStripLabel toolStripLabel)
        {
            <WMToolStripLabel model=@toolStripLabel></WMToolStripLabel>
        }
        @if (item is ToolStripButton toolStripButton)
        {
            <WMToolStripButton model=@toolStripButton></WMToolStripButton>
        }
        @if (item is ToolStripSeparator toolStripSeparator)
        {
            <WMToolStripSeparator model=@toolStripSeparator></WMToolStripSeparator>
        }
    }
</TelerikToolBar>

<style>
    [email protected]() {
        padding: 1px;
        align-items: stretch;
    }

    .@(model.GetComponentClass() + ".AutoSize") {
        height: auto;
    }
</style>
[email protected]() {
    padding: 1px;
    align-items: stretch;
}

[email protected]().AutoSize {
    height: auto;
}
@if (this.Visible)
{
    <div class="@(this.TabControl.GetStringClasses())">
        <WMStyleBase [email protected]></WMStyleBase>

        <TelerikTabStrip Class="@(this.GetAppearanceStringClass())"
                         Height="100%"
                         ActiveTabIndex="this.GetActiveTabIndex()"
                         ActiveTabIndexChanged="TabChangedHandler"
                         TabPosition="(TabPosition)this.TabControl.Alignment"
                         Scrollable="!this.TabControl.Multiline"
                         TabAlignment="this.GetTabAlignmentFromSizeMode()">
            @ChildContent
        </TelerikTabStrip>

        <style>
            .@(this.TabControl.GetComponentClass()) .k-item {
                font-size: @this.TabControl.Font.GetFontSize();
                font-weight: @this.TabControl.Font.GetFontWeight();
                font-style: @this.TabControl.Font.GetFontStyle();
                font-family: @this.TabControl.Font.GetFontFamily();
                text-decoration: @this.TabControl.Font.GetTextDecoration();
            }

            .@(this.TabControl.GetComponentClass()) .k-item {
                height: @this.GetItemSizeHeight();
                width: @this.GetItemSizeWith();
            }

            .@(this.TabControl.GetComponentClass()) .k-tabstrip-items-wrapper > .k-button {
                height: @this.GetItemSizeHeight();
            }
        </style>
    </div>
}
[email protected]() .k-item {
    font-size: [from model];
    font-weight: [from model];
    font-style: [from model];
    font-family: [from model];
    text-decoration: [from model];
    height: [calculated];
    width: [calculated];
}

[email protected]() .k-tabstrip-items-wrapper > .k-button {
    height: [calculated];
}

ToolStripButton

Description

This component represents a toolbar button in Blazor using the Gap.Blazor.ToolStripButton model. It is rendered inside a Telerik ToolBarButton and supports displaying an image, text, or both. The component also supports tooltips, autosizing, and click interaction.

Usage

<WMStyleBase model=@toolStripButton></WMStyleBase>

<ToolBarButton Class="@($"{toolStripButton.GetStringClasses()} {toolStripButton.GetToolTipClass()} {GetAutoSizeClass()}")"
               Title="@toolStripButton.GetToolTipText()"
               OnClick="@toolStripItemClick"
               Visible="this.Visible">
    @if (this.ShowImage && this.GetImage() is string image)
    {
        <div>
            <img class="icon-size-default" src="@image" />
        </div>
    }
    @if (this.ShowText && !string.IsNullOrEmpty(this.Text))
    {
        <div>
            <p class="button-text-default">@toolStripButton.Text</p>
        </div>
    }
</ToolBarButton>

<style>
    [email protected]() {
        position: relative;
        display: block;
        padding: 2px;
    }

    .@(model.GetComponentClass() + ".AutoSize") {
        width: auto;
        height: auto;
    }

    [email protected]() .icon-size-default {
        display: block;
        margin-inline: auto;
        float: @this.toolStripButton.ImageAlign.GetFloatStyle();
    }

    [email protected]() .button-text-default {
        margin: 1px;
        clear: both;
        float: @this.toolStripButton.TextAlign.GetFloatStyle();
    }
</style>

Properties

  • toolStripButton: Instance of the Gap.Blazor.ToolStripButton model.

  • DisplayStyle: Determines whether to show image, text, or both (Image, Text, ImageAndText).

  • ToolTipText: Tooltip shown on hover.

  • Image: The image displayed on the button.

  • Text: The label text displayed on the button.

  • ImageAlign / TextAlign: Control the float alignment of the image and text.

Methods

  • OnInitialized(): Subscribes to NotifyModelChanged and ImageChanged to update the UI when the model changes.

  • toolStripItemClick(MouseEventArgs): Invokes the PerformClick() method on the model when the button is clicked.

  • GetImage(): Retrieves the image source from the model.

  • ShowImage / ShowText: Boolean flags derived from DisplayStyle to control rendering.

Dynamic Rendering

  • ToolBarButton: Renders the button UI.

  • Image and Text: Conditionally rendered based on DisplayStyle.

  • CSS Styling: Dynamically applies layout and alignment styles.

Styles

[email protected]() {
    position: relative;
    display: block;
    padding: 2px;
}

[email protected]().AutoSize {
    width: auto;
    height: auto;
}

[email protected]() .icon-size-default {
    display: block;
    margin-inline: auto;
    float: [based on ImageAlign];
}

[email protected]() .button-text-default {
    margin: 1px;
    clear: both;
    float: [based on TextAlign];
}

Events

  • Click: Triggered when the button is clicked.

  • ImageChanged: Updates the UI when the image changes.

  • NotifyModelChanged: Triggers a full UI refresh when the model changes.

DCP: Desktop Compatibility Library

Assessment Tool

Issue with using "MessageBox.Show" in Blazor

Issue to be solve

In Winforms you can have a constructor for a Form, and call a MessageBox.Show or a method that calls the MessageBox.Show

In Blazor, we need to wait for the MessageBox response(in most cases) to continue execution. Why most cases?

As an example, if we have a MessageBox.Show(“Message”), this is just an Expression Statement. We don’t need to wait for a response because it is just a notification, this means that the application does not need to wait for a response.

Proposed solution

What if we just show a notification in a web oriented way? Well, we can use the TelerikNotification component.

This allow us to just show the notification without having to wait for the result or response.

So to achieve this, it is needed to have an event or service to notify TelerikNotification to show and use the correct message. And, by migration, we need to replace the MessageBox.Show by a different statement. The approach is to have a TelerikNotification component in the WMApp component, and this control will be listening for a subscription to show the message

Blazor Desktop Compativility Platform

In the Application.cs, we are building an event to invoke the ShowNotification.

In the migrated code, we can replace a simple MessageBox.Show by a Application.CurrentApplication.ShowNotification()

By doing this, we don’t need to change anything else (initially) in the migrated razor files. Just a replace during migration (and using the DCP changes)

This should be the result when showing the notification in an web environment application:

Blazor DCP: Gap.Blazor.Application Class Reference

The Application All Applications of any Desktop Compatibility Platform should associate the Forms with the corresponding IApplication implementation.

The Application All Applications of any Desktop Compatibility Platform should associate the Forms with the corresponding IApplication implementation.

<mapfrom>System.Windows.Forms.Application.</mapfrom>

Dispose()

Releases unmanaged and - optionally - managed resources.

Parameters

RemoveActiveForm()

Removes the active form.

Parameters

ActiveForm

Gets or sets the active form.

The active form.

ActiveForms

Gets or sets the active forms.

The active forms.

CurrentApplication

Gets the current application.

The current application.

ItemHasChanged

Gets or sets the item has changed.

The item has changed.


The documentation for this class was generated from the following file:

  • src/Gap.Blazor/Controls/Application.cs

Panel

Description

This component represents a panel container in Blazor using the Gap.Blazor.Panel model. It provides a styled container for grouping other components or content, with support for dynamic border styles, scroll behavior, and mouse interaction events.

Usage

Properties

  • panel: Instance of the Gap.Blazor.Panel model.

  • elementRef: Reference to the rendered DOM element (used for interop or focus).

  • ChildContent: The content rendered inside the panel.

Methods

  • OnInitialized(): Subscribes to model events:

    • BorderStyleChanged

    • AutoScrollChanged

  • GetBorderCss(): Returns the CSS border style based on the model’s BorderStyle, only if the panel has a defined Width and Top position.

Dynamic Rendering

  • WMStyleBase: Applies base styles from the model.

  • CSS Classes: Dynamically generated using panel.GetStringClasses() and model.GetComponentClass().

  • Mouse Events: @onmouseup and @onmousemove handlers are attached for interaction support.

Styles

Events

  • BorderStyleChanged: Triggers UI update when the border style changes.

  • AutoScrollChanged: Triggers UI update when scroll behavior changes.

@if (this.Visible)
{   
    <WMStyleBase model=@panel></WMStyleBase>
    <div class="@this.panel.GetStringClasses()" 
         @ref="elementRef" 
         @onmouseup="MouseUpHandler" 
         @onmousemove="MouseMoveHandler">
        @ChildContent
    </div>

    <style>
        [email protected]() {           
            border: @this.GetBorderCss();
            overflow: @this.panel.GetAutoScrollStyle();
        }
    </style>
}
[email protected]() {
    border: [based on BorderStyle];
    overflow: [based on AutoScroll];
}

Public Member Functions

Application ()

Initializes a new instance of the Application class.

void

Dispose ()

void

RemoveActiveForm (Form form)

Removes the active form.

Protected Member Functions

virtual void

Dispose (bool disposing)

Releases unmanaged and - optionally - managed resources.

Properties

static Application

CurrentApplication [get]

Gets the current application.

List< Form >

ActiveForms [get, set]

Gets or sets the active forms.

Form

ActiveForm [get, set]

Gets or sets the active form.

EventHandler

ItemHasChanged [get, set]

Gets or sets the item has changed.

virtual void Gap.Blazor.Application.Dispose

(

bool

disposing

)

protectedvirtual

virtual void Gap.Blazor.Application.Dispose

(

bool

disposing

)

disposing

true to release both managed and unmanaged resources; false to release only unmanaged resources.

void Gap.Blazor.Application.RemoveActiveForm

(

Form

form

)

Form Gap.Blazor.Application.ActiveForm

getset

List<Form> Gap.Blazor.Application.ActiveForms

getset

EventHandler Gap.Blazor.Application.ItemHasChanged

getset

More...
◆
◆
◆
◆
◆
◆

TextBox

Description

This component represents a text input field in Blazor using the Gap.Blazor.TextBox model. It supports both single-line and multi-line input, password masking, scroll bar configuration, and selection tracking. It uses Telerik’s TextBox and TextArea components and integrates JavaScript interop for advanced selection handling.

Usage

@if (model.Visible)
{
    <WMStyleBase model=@textBox></WMStyleBase>

    <div @ref="inputWrapperRef" @onkeydown="@keyDownHandler" @onkeypress="@keyPressHandler" @onkeyup="@keyUpHandler">
        @if (textBox.Multiline)
        {
            <TelerikTextArea Enabled="@this.Enabled"
                             ReadOnly="@this.ReadOnly"
                             class="@($"{model.GetStringClasses()} {this.GetScrollBarClass()}")"
                             Value="@Text"
                             ValueChanged="@valueChange"
                             DebounceDelay="0"
                             TabIndex=@TabIndex
                             OnBlur="@OnBlurHandler"
                             @ref="elementRef"
                             ResizeMode="@TextAreaResizeMode.None" />
        }
        else
        {
            <TelerikTextBox Enabled="@this.Enabled"
                            Password="@GetPassword()"
                            ReadOnly="@this.ReadOnly"
                            class="@model.GetStringClasses()"
                            Value="@Text"
                            ValueChanged="@valueChange"
                            DebounceDelay="0"
                            @ref="elementRef"
                            TabIndex=@TabIndex
                            OnBlur="@OnBlurHandler" />
        }
    </div>

    <style>
        [email protected]() {
            border: @GetBorderCss();
        }
        [email protected]() input {
            text-align: @this.textBox.TextAlign.GetTextAlignStyle();
        }
        .no-scrollbar textarea {
            overflow-y: hidden !important;
        }
        .horizontal-scrollbar textarea {
            overflow-y: scroll !important;
        }
    </style>
}

Properties

  • textBox: Instance of the Gap.Blazor.TextBox model.

  • Multiline: Determines whether to render a TextArea or TextBox.

  • PasswordChar: If set, masks input as a password.

  • ScrollBars: Controls vertical scroll behavior (None, Horizontal).

  • SelectionStart / SelectionLength: Used for tracking and setting text selection.

  • TextAlign: Controls horizontal text alignment.

  • ReadOnly: Indicates if the input is editable.

Methods

  • OnInitialized(): Subscribes to TextAlignChanged to update alignment dynamically.

  • Focus(): Focuses the input and applies selection range.

  • ApplyTextBoxSelection(): Uses JS interop to set selection range.

  • HandleSelectionChange(start, end): Updates the model when selection changes.

  • valueChange(string): Updates the model’s text value unless a keypress was already handled.

  • GetPassword(): Returns true if PasswordChar is set.

  • GetScrollBarClass(): Returns CSS class based on scroll bar configuration.

  • GetBorderCss(): Returns border style based on model dimensions.

JavaScript Integration

function applyTextBoxSelection(inputWrapper, selectionStart, selectionEnd) { ... }
function addSelectionChangeListeners(inputWrapper, textBoxComponent) { ... }
  • applyTextBoxSelection: Sets the selection range in the input.

  • addSelectionChangeListeners: Tracks selection changes and deletion keys.

Dynamic Rendering

  • TelerikTextBox / TelerikTextArea: Rendered based on Multiline.

  • CSS Styling: Dynamically applies border, alignment, and scroll behavior.

  • JS Interop: Enables advanced selection tracking and manipulation.

Events

  • TextAlignChanged

  • SelectionChange (via JS interop)

  • OnBlur

  • ValueChanged

DateTimePicker

Description

This component represents a date and time picker in Blazor using the Gap.Blazor.DateTimePicker model. It integrates with Telerik’s DatePicker and optionally includes a checkbox to enable or disable the date selection. The component supports dynamic formatting and styling based on the model.

Usage

@using Gap.Blazor
@using Gap.Blazor.Components
@using Telerik.Blazor.Components
@inherits WMControlComponentBase
@namespace Gap.Blazor.Components

<WMStyleBase model=@dateTimePicker></WMStyleBase>
<div class="@dateTimePicker.GetStringClasses()">
    @if(dateTimePicker.ShowCheckBox == true)
    {
        <TelerikCheckBox @bind-Value="@dateTimePicker.Checked"></TelerikCheckBox>
    }
    <TelerikDatePicker class="@(!dateTimePicker.Checked ? "k-unchecked" : "" )"
                       @bind-Value="@dateTimePicker.Value" 
                       Format="@convertToTelerikFormat()" 
                       OnChange=@dateTimePickerOnChange 
                       OnOpen="@updateCheckedValue">
    </TelerikDatePicker>
</div>

<style>
    [email protected]() {
        display: inline-flex;
    }
    [email protected]() .k-checkbox-wrap {
        margin-top: 5px;
    }
    [email protected]() .k-input-inner {
        font-size: @dateTimePicker.Font.GetFontSize();
    }
    [email protected]() .k-input-button {
        width: 20px;
    }
    .k-datetimepicker.k-unchecked .k-input-inner {
        pointer-events: none;
        opacity: 0.6;
        filter: grayscale(0.1);
    }
</style>

Properties

  • DateTimePicker: Instance of the Gap.Blazor.DateTimePicker model.

  • Value: Bound to the selected date/time value.

  • Checked: Boolean indicating whether the date picker is enabled (used with ShowCheckBox).

  • Format: Determines the display format of the date/time.

Methods

  • OnInitialized(): Subscribes to the ValueChanged event to trigger UI updates.

  • DateTimePickerOnChange(): Invokes the model’s PerformValueChange() method.

  • UpdateCheckedValue(): Automatically sets Checked = true when the calendar is opened.

  • ConvertToTelerikFormat(): Converts the model’s format to a Telerik-compatible string:

    • Long → "dddd, MMMM dd, yyyy"

    • Short → "M/d/yyyy"

    • Time → "h:mm tt"

    • Custom → Uses CustomFormat from the model

Dynamic Rendering

The component dynamically renders a date picker with optional checkbox based on the DateTimePicker model:

  • WMStyleBase: Applies styles based on the model.

  • TelerikCheckBox: Toggles the enabled state of the date picker.

  • TelerikDatePicker: Displays the date/time selection UI.

Styles

  • Layout: Uses inline-flex for alignment.

  • Checkbox: Margins adjusted for visual alignment.

  • Input Styling: Font size and button width are model-driven.

  • Disabled State: When unchecked, the date picker is visually disabled using opacity and pointer restrictions.

Events

  • ValueChanged: Triggered when the selected date/time changes.

Mdi Container Component

Description

This component represents an MDI (Multiple Document Interface) container in Blazor. It dynamically renders a collection of child forms (Gap.Blazor.Form) using Blazor’s DynamicComponent, allowing multiple forms to be displayed and managed within a single parent container.

Usage

@namespace Gap.Blazor.Components
@using Gap.Blazor

<div style="position: relative; width: 100%; height: 100%;" id="mdi-container">
    @if (MdiChildren != null)
    {
        foreach (var MdiChild in MdiChildren)
        {
            var formArgs = new Dictionary<string, object>();
            formArgs.Add("model", MdiChild);
            <DynamicComponent @key="MdiChild" Type="@MdiChild.View" Parameters="@formArgs"></DynamicComponent>
        }
    }
</div>

Properties

  • MdiChildren: A list of Gap.Blazor.Form instances representing the child forms to be rendered within the MDI container.

Dynamic Rendering

  • DynamicComponent: Used to render each child form dynamically based on its View type.

  • @key Directive: Ensures proper rendering and diffing of each child form.

  • Container Styling: The outer <div> uses position: relative and full width/height to serve as a layout surface for child forms.

Behavior

  • Each child form is rendered with its own model passed via the Parameters dictionary.

  • The component supports any number of child forms and updates automatically when MdiChildren changes.

Deploying SKS Blazor migrated app to Azure Container Instances

This document provides a detailed guide for deploying SKS Blazor migrated applications using Azure Container Instances (ACI). It covers the necessary steps for setting up a local Docker environment, building container images, defining base images, and pushing images to container registries, specifically GitHub Container Registry (GHCR) and Azure Container Registry (ACR). Additionally, it includes instructions for creating and running containers in Azure, along with important considerations for ensuring successful deployment, particularly regarding the choice of base images and runtime dependencies for Windows-based applications.

Installing Docker local environment.

To first build a Docker image with the SKS application is important to install and configure a local Docker runtime environment.

  1. Add containers and hyper-v features on windows:

    1. Open Windows Features:

      1. Press Win + R, type optional features, and press Enter.

      2. Or, search for "Turn Windows features on or off" in the Start menu and open it.

    2. Enable Containers:

      1. In the Windows Features dialog, scroll down and check the box for Containers.

    3. Enable Hyper-V:

      1. In the same list, find and check Hyper-V (expand it and ensure both "Hyper-V Management Tools" and "Hyper-V Platform" are checked).

    4. Apply and Restart:

      1. Click OK.

      2. Windows will search for required files and apply changes.

      3. Restart your computer when prompted.

  2. Download Docker Desktop from: https://www.docker.com/products/docker-desktop

  3. Run the installer (Docker Desktop Installer.exe)

  4. During installation:

    1. Enable "Install required Windows components for WSL 2"

    2. Enable "Use Windows containers instead of Linux containers"

  5. Click "Ok" to install

  6. Wait for installation to complete

  7. Restart your computer

  8. Run Docker desktop application.

  9. Right click on Docker tray icon and select switch to windows containers (Since the images used to build the WebMap Blazor images are Windows OS based it’s necessary to work with Windows containers).

  10. In settings verify that desktop-windows is running

How to build a Windows image with SKS app.

For this exercise the SKS application was used. You can find the application along with the Dockerfile in the repo: https://collaboration.artinsoft.com/tfs/Product/Product/_git/BlazorDemos

The branch with the changes and the Dockerfile is: support/DockerContainerWindowsSKS.

Before creating the Dockerfile it’s important to define the base image used to create the container.

Defining a base image

There a are 3 different types of base image the election should be based on the requirements:

  • Linux Images

    • Base OS: Linux (usually Debian or Alpine).

    • Image Example: mcr.microsoft.com/dotnet/aspnet:9.0

    • Size: Smallest.

    • Performance: Fast startup, low resource usage.

    • Compatibility:

      • Runs on Linux hosts (including most cloud/container platforms).

      • Cannot run Windows-specific workloads (e.g., Windows Forms, WPF, COM).

    • Best for:

      • Web apps (Blazor, ASP.NET Core, APIs).

      • Cloud-native deployments (Azure Container Apps, Kubernetes, Docker Hub).

  • Nano Server Images

    • Base OS: Windows Nano Server (minimal Windows kernel).

    • Image Example: mcr.microsoft.com/dotnet/aspnet:9.0-nanoserver-1809

    • Size: Smaller than Server Core, larger than Linux.

    • Performance: Good, but not as fast or small as Linux.

    • Compatibility:

      • Runs only on Windows hosts (Windows containers).

    • No support for full .NET Framework, Windows Forms, WPF, or GDI+.

    • Only supports .NET Core/.NET 5+ console and web apps.

    • Best for:

      • .NET Core web apps that require Windows but not full Windows APIs.

  • Server Core Images

    • Base OS: Windows Server Core (more complete Windows environment).

    • Image Example: mcr.microsoft.com/dotnet/aspnet:9.0-windowsservercore-ltsc2022

    • Size: Largest.

    • Performance: Slower startup, higher resource usage.

    • Compatibility:

      • Runs only on Windows hosts (Windows containers).

      • Supports full .NET Framework, Windows Forms, WPF, GDI+, COM, etc.

    • Best for:

      • Legacy .NET Framework apps.

      • Apps needing full Windows APIs (desktop, GUI, COM, etc.).

Since most migrations come from windows architecture usually they are tightly coupled to windows environments, also WebMap Blazor code is compiled and designed to target Windows Desktop runtime that’s why the selected base image for the SKS POC is Server Core Images.

Once the based image is defined, a Dockerfile should be created to perform the image build. The Dockerfile used for this exercise can be found in the branch mentioned above.

After creating the Dockerfile, open a command line window pointing to the same folder where the dockerfile is and run the build command to generate an image.

docker build --no-cache -t <desired-image-name>:<version>

You should have the image created in the docker desktop UI.

Then you can run the image locally as a container using the docker run command.

docker run -p 8080:8080 -p 8081:8081 --name <desired-container-name> <image-name-used-build-command>

There should be a new container named sks_blazor_container created

Since one of the ports exposed when running the container is 8080. The app will listen in http://localhost:8080

Next step is to push the image to a Container registry so it can be run from the Azure Container instances app. But first you need to define which container registry to use.

Define container registry

These are some of the most common container registry options:

  • Git Hub Container Registry

    • Free for public repositories and generous free tier for private images.

    • No extra Azure cost (unlike ACR, which may incur charges after the free tier).

    • Easy integration with GitHub Actions for CI/CD.

    • Widely supported by Azure Container Apps and Azure Web App for Containers.

    • No vendor lock-in—you can use the same registry for other platforms.

  • Docker Hub

    • Free for public images (rate limits apply).

    • Simple to use and supported everywhere.

    • Private repositories are limited to the free tier.

  • Azure Container Registry

    • If you need enterprise features (private networking, managed identities, geo-replication).

    • If you want tight Azure integration and are comfortable with potential costs.

    • https://azure.microsoft.com/en-us/pricing/details/container-registry/

  • Using GHCR to push the image to the remote repository

    • Go to GitHub.com → Settings → Developer Settings → Personal Access Tokens → Tokens (classic).

    • Click "Generate new token (classic)"

    • Select the following scopes:

      • read:packages

      • write:packages

      • delete:packages (if you need to delete containers)

    • Copy the generated token

    • Login the local docker environment to GHCR:

      • echo <GENERATED-TOKEN>| docker login ghcr.io -u <YOUR_GITHUB_USERNAME> --password-stdin
    • Tag the image in GHCR:

      • docker tag <image-name>:<version> ghcr.io/<YOUR_GITHUB_USERNAME>/<image-name>:<version>
    • Push the image

      • docker push ghcr.io/<YOUR_GITHUB_USERNAME>/<image-name>:<version>
  • Using ACR to push the image to the remote repository

    • Open the Azure Portal

      • Go to https://portal.azure.com and sign in with your Azure account.

    • Open Azure Cloud Shell

      • In the top-right corner of the portal, click the Cloud Shell icon (it looks like a terminal or command prompt).

      • When prompted, select Bash as your shell environment.

      • If this is your first time, you may be asked to create a storage account—follow the prompts to set it up.

    • Create a resource group.

    az group create --name <resource-group-name> --location <location>
    • Create ACR

    az acr create --resource-group <resource-group-name> --name <acr-name> --sku Basic
    • Go back to you local environment, open a cmd and login to azure.

    az login
    • Login to acr.

    az acr login --name <acr-name>
    • Tag the image.

    docker tag <image-name>:<version> <acr-name>.azurecr.io/<image-name>:<version>
    • Push the image.

    docker push <acr-name>.azurecr.io/<image-name>:<version>

Create an Azure Container instance that runs the pushed image.

  1. Open the Azure Portal

    1. Go to https://portal.azure.com and sign in with your Azure account.

  2. Open Azure Cloud Shell

    1. In the top-right corner of the portal, click the Cloud Shell icon (it looks like a terminal or command prompt).

    2. When prompted, select Bash as your shell environment.

    3. If this is your first time, you may be asked to create a storage account—follow the prompts to set it up.

  3. Create a resource group (If you don't have one).

az group create --name <resource-group-name> --location <location>
  1. Create Azure Container Instances.

    1. Using GHCR:

    az container create   --resource-group <resource-group-name>   
    --name <aci-name> 
     --image ghcr.io/<YOUR_GITHUB_USERNAME>/<image-name>:<version>   --registry-login-server ghcr.io   
    --registry-username <YOUR_GITHUB_USERNAME>  --registry-password <GENERATED-TOKEN>   
    --dns-name-label <desired-site-name>  --ports 8080 8081 
    --os-type Windows 
    --cpu 2 
    --memory 4
    1. Using ACR:

      1. Go back to Azure Portal

      2. Navigate to Your Container Registry

        In the left sidebar, select "All services" and search for "Container registries".

        Click on your desired ACR instance from the list.

      3. Access the Access Keys Section

        In the ACR blade, find and select "Access keys" in the left menu.

      4. View or Enable Admin User

        By default, the Admin user is disabled for security reasons.

        To generate access keys, toggle Admin user to Enabled.

      5. Copy Username and Passwords

        After enabling, you will see:

        Username (usually the registry name)

        Password and Password2 (two interchangeable passwords)

az container create   --resource-group <resource-group-name>  
--name <aci-name>  
--image <acr-name>.azurecr.io/<image-name>:<version>   --registry-login-server <acr-name>.azurecr.io   
--registry-username <username-from-accesskeys>  
--registry-password <password-from-accesskeys> 
--dns-name-label <desired-site-name> 
 			--ports 8080 8081 
--os-type Windows --cpu 2 
--memory 4
  1. Navigate to the deployed app. In any browser navigate to: http:// ..azurecontainer.io:/

Important considerations

Current SKS app requires a to run in a mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022 image because migrated app and dcp classes are compiled using Microsoft.WindowsDesktop.App runtime:

This tells .NET to target the Windows Desktop runtime, which is not included in the ASP.NET Core runtime images (mcr.microsoft.com/dotnet/aspnet).

The aspnet image only includes Microsoft.AspNetCore.App and Microsoft.NETCore.App, not Microsoft.WindowsDesktop.App.

Under normal circumstances build and runtime images are different for optimization purposes:

  • For build: mcr.microsoft.com/dotnet/sdk:9.0-windowsservercore-ltsc2022

  • For runtime: mcr.microsoft.com/dotnet/aspnet:9.0-windowsservercore-ltsc2022

Windows Desktop apps (WPF/WinForms) are not designed to run in containers—they require a Windows session and often access the UI, which is not available in containers. Blazor projects should not require the Windows Desktop runtime unless you are using desktop-specific APIs.

Telerik.UI for Blazor Trial is currently being obtained from a local repository because windows image cannot access https://ais-build-w7.artinsoft.com:8625/nuget/researchExternal/v3/index.json and public feed doesn’t have the required version https://nuget.telerik.com/v3/index.json . The issue with ais probably has to do with network security constraints such as firewalls, antiviruses or certificates. Possible solutions: consume the package from a public feed such as packages.mobilize or fix the network connection issue between the container and ais server.