Skip to main content
All docs
V26.1
  • Implement a Property Editor Based on a JavaScript Widget (Blazor)

    • 6 minutes to read

    This topic explains how to implement a Property Editor for ASP.NET Core Blazor when the underlying control is a JavaScript widget.

    This approach requires a JavaScript interop layer in addition to the Razor component and Component Model. The JavaScript module creates the widget, keeps it in sync with the property value, and cleans up when the editor is removed.

    If the control is a Razor component instead, refer to the following topic: Implement a Property Editor Based on a Custom Razor Component (Blazor).

    The following example implements a Property Editor using the DevExtreme dxSlider widget. The widget displays a horizontal track with a draggable handle and a hover tooltip showing the current percentage. If a user drags the handle or clicks the track, the editor writes the new value back to the business object in real time.

    XAF ASP.NET Core Blazor Custom JS Component-Based Numeric Property Editor in a Detail View, DevExpress

    Note

    The DevExpress.ui namespace is available globally because XAF Blazor loads the DevExtreme bundle on every page. No additional script references are required.

    Implement the following entities in the application project (SolutionName.Blazor.Server):

    JavaScript Module
    A JS module that creates the dxSlider, updates its value, and handles disposal.
    Razor Component
    A Razor markup file that holds the container <div>, imports the JS module, and manages its lifecycle.
    Component Model
    An object with properties that match the names of the Razor component’s declared parameters.
    Property Editor
    A class that integrates the component into your XAF application.

    JavaScript Module

    Create a JavaScript module with four exported functions:

    • initialize — creates a dxSlider instance on the page. It sets the value range, read-only state, and a tooltip that shows the current value as a percentage on hover. It also registers an onValueChanged handler that notifies .NET when the user moves the slider.
    • setValue — updates the value of an existing dxSlider instance.
    • setReadOnly — updates the read-only state of an existing dxSlider instance.
    • dispose — destroys the dxSlider instance and removes it from the page.

    File: SolutionName.Blazor.Server\wwwroot\js\progressSlider.js

    const percentFormat = v => `${v}%`;
    
    export function initialize(containerId, dotNetRef, value, readOnly) {
        const el = document.getElementById(containerId);
        if (!el || DevExpress.ui.dxSlider.getInstance(el)) return;
    
        new DevExpress.ui.dxSlider(el, {
            min: 0,
            max: 100,
            step: 1,
            value,
            readOnly,
            tooltip: {
                enabled: true,
                showMode: 'onHover',
                format: percentFormat
            },
            label: {
                visible: true,
                position: 'bottom',
                format: percentFormat
            },
            onValueChanged: ({ value }) => {
                dotNetRef.invokeMethodAsync('UpdateValue', value);
            }
        });
    }
    
    export const setValue = (containerId, value) => setOption(containerId, 'value', value);
    export const setReadOnly = (containerId, readOnly) => setOption(containerId, 'readOnly', readOnly);
    export const dispose = containerId => getInstance(containerId)?.dispose();
    
    function setOption(containerId, key, value) {
        getInstance(containerId)?.option(key, value);
    }
    
    function getInstance(containerId) {
        const el = document.getElementById(containerId);
        return el ? DevExpress.ui.dxSlider.getInstance(el) : null;
    }
    

    Razor Component

    1. Create a Razor component named DxSliderEditor.razor.
    2. Render a single <div> with a unique ID. JavaScript creates the slider inside this element.
    3. On the first render, load the JS module and call initialize. Pass a DotNetObjectReference so that JavaScript can notify .NET when the user moves the slider.
    4. On subsequent renders, call setValue or setReadOnly only when those values have changed.
    5. Implement IAsyncDisposable to destroy the widget and release both the module reference and the DotNetObjectReference when the component is removed.

      File: SolutionName.Blazor.Server\Editors\DxSliderEditor.razor

      @namespace SolutionName.Blazor.Server.Editors
      @implements IAsyncDisposable
      
      <div id="@containerId" class="xaf-slider-editor"></div>
      
      @code {
          [Inject] private IJSRuntime JSRuntime { get; set; }
      
          [Parameter] public int Value { get; set; }
          [Parameter] public EventCallback<int> ValueChanged { get; set; }
          [Parameter] public bool ReadOnly { get; set; }
      
          private readonly string containerId = "xaf-" + Guid.NewGuid().ToString("N");
      
          private IJSObjectReference _jsModule;
          private DotNetObjectReference<DxSliderEditor> dotNetRef;
      
          private int _previousValue;
          private bool _previousReadOnly;
      
          protected override void OnInitialized()
          {
              dotNetRef = DotNetObjectReference.Create(this);
              _previousValue = Value;
              _previousReadOnly = ReadOnly;
          }
      
          protected override async Task OnAfterRenderAsync(bool firstRender)
          {
              if (firstRender)
              {
                  await JSRuntime.LoadDxResources();
                  _jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "/js/progressSlider.js");
                  await _jsModule.InvokeVoidAsync("initialize", containerId, dotNetRef, Value, ReadOnly);
                  return;
              }
      
              if (_jsModule is null) return;
      
              if (Value != _previousValue)
              {
                  _previousValue = Value;
                  await _jsModule.InvokeVoidAsync("setValue", containerId, Value);
              }
      
              if (ReadOnly != _previousReadOnly)
              {
                  _previousReadOnly = ReadOnly;
                  await _jsModule.InvokeVoidAsync("setReadOnly", containerId, ReadOnly);
              }
          }
      
          [JSInvokable]
          public async Task UpdateValue(int value)
          {
              _previousValue = value;
              Value = value;
              await ValueChanged.InvokeAsync(value);
          }
      
          public async ValueTask DisposeAsync()
          {
              if (_jsModule is not null)
              {
                  try { await _jsModule.InvokeVoidAsync("dispose", containerId); }
                  catch (JSDisconnectedException) { }
                  catch (TaskCanceledException) { }
      
                  await _jsModule.DisposeAsync();
              }
              dotNetRef?.Dispose();
          }
      }
      
    6. In the Properties window, set this file’s Build Action to Content.

    Component Model

    1. Create a ComponentModelBase descendant named DxSliderModel.cs.
    2. Declare properties with the same names and types as the parameters in DxSliderEditor.razor.
    3. Override the ComponentType property to return typeof(DxSliderEditor).

    File: SolutionName.Blazor.Server\Editors\DxSliderModel.cs

    using DevExpress.ExpressApp.Blazor.Components.Models;
    using Microsoft.AspNetCore.Components;
    
    namespace SolutionName.Blazor.Server.Editors;
    
    public class DxSliderModel : ComponentModelBase {
        public int Value {
            get => GetPropertyValue<int>();
            set => SetPropertyValue(value);
        }
        public EventCallback<int> ValueChanged {
            get => GetPropertyValue<EventCallback<int>>();
            set => SetPropertyValue(value);
        }
        public bool ReadOnly {
            get => GetPropertyValue<bool>();
            set => SetPropertyValue(value);
        }
        public override Type ComponentType => typeof(DxSliderEditor);
    }
    

    Property Editor

    1. Create a class named DxSliderPropertyEditor.cs that inherits from BlazorPropertyEditorBase.
    2. Add the [PropertyEditor(typeof(int), false)] attribute to assign this editor to int properties. The false argument means that this is not the default editor. You have to assign it explicitly in the Application Model.
    3. Override CreateComponentModel to create a DxSliderModel and define what happens when the user moves the slider: update the model value and save it to the business object.
    4. Override ReadValueCore to pass the current value from the business object to the slider. Use Convert.ToInt32 to safely convert the value before passing it.
    5. Override ApplyReadOnly to disable the slider when the property is not editable. The null check prevents a crash during startup, because XAF calls this method before the component is ready.

      File: SolutionName.Blazor.Server\Editors\DxSliderPropertyEditor.cs

      using DevExpress.ExpressApp.Blazor.Components.Models;
      using DevExpress.ExpressApp.Blazor.Editors;
      using DevExpress.ExpressApp.Editors;
      using DevExpress.ExpressApp.Model;
      using Microsoft.AspNetCore.Components;
      
      namespace SolutionName.Blazor.Server.Editors;
      
      [PropertyEditor(typeof(int), false)]
      public class DxSliderPropertyEditor : BlazorPropertyEditorBase {
          public DxSliderPropertyEditor(Type objectType, IModelMemberViewItem model) : base(objectType, model) { }
          public override DxSliderModel ComponentModel => (DxSliderModel)base.ComponentModel;
      
          protected override IComponentModel CreateComponentModel() {
              var model = new DxSliderModel();
              model.ValueChanged = EventCallback.Factory.Create<int>(this, value => {
                  model.Value = value;
                  OnControlValueChanged();
                  WriteValue();
              });
              return model;
          }
      
          protected override void ReadValueCore() {
              base.ReadValueCore();
              ComponentModel.Value = Convert.ToInt32(PropertyValue);
          }
      
          protected override object GetControlValueCore() => ComponentModel.Value;
      
          protected override void ApplyReadOnly() {
              base.ApplyReadOnly();
              if (ComponentModel != null) {
                  ComponentModel.ReadOnly = !AllowEdit;
              }
          }
      }
      
    6. Rebuild your solution and double-click the SolutionName.Blazor.Server\Model.xafml to invoke the Model Editor for the ASP.NET Core Blazor application project.

    7. Navigate to BOModel | <Class> | OwnMembers | <Member> and set the PropertyEditorType property to DxSliderPropertyEditor.

      Tip

      You can also set this Property Editor for a specific View only. To do this, specify the Views | <DetailView> | Items | <PropertyEditor> node’s PropertyEditorType property.

    Additional Customizations

    The following techniques described in Implement a Property Editor Based on a Custom Component (Blazor) also apply to this approach:

    Access XafApplication and ObjectSpace
    If the Property Editor requires access to the application or the View ObjectSpace, implement the IComplexViewItem interface. Use IComplexViewItem.Setup to obtain the XafApplication and IObjectSpace objects.
    Display a Custom Component in a List View
    By default, the Property Editor renders the same component in Detail Views and during inline editing in List Views. To specify a different component for regular List View cells, override the BlazorPropertyEditorBase.CreateViewComponentCore method.
    See Also