Skip to main content
All docs
V22.1
.NET Standard 2.0+

How to: Use a Custom Component to Implement List Editor (Blazor)

  • 10 minutes to read

This topic describes how to implement a custom List Editor that shows images in an ASP.NET Core Blazor application. The List Editor displays a Razor component with custom objects. These objects implement a custom IPictureItem interface to store images and their captions.

View Example: How to: Use a Custom Component to Implement List Editor (Blazor)

Blazor Custom List Editor

To add a custom List Editor to your ASP.NET Core Blazor application, define the required data model and implement the following components in the ASP.NET Core Blazor module project (MySolution.Module.Blazor). If your solution does not contain this project, add these components to the application project (MySolution.Blazor.Server).

Prerequisites

Before you start, ensure that the SDK of the MySolution.Module.Blazor project is Microsoft.NET.Sdk.Razor.

File: MySolution.Module.Blazor.csproj.

<Project Sdk="Microsoft.NET.Sdk.Razor">
  <!-- ... -->
</Project>

Define the Data Model

  1. In the MySolution.Module project, create a new interface and name it IPictureItem. In this interface, declare the Image and Text properties. This allows the List Editor to work with different types of objects that implement this interface.

    File: MySolution.Module\BusinessObjects\IPictureItem.cs

    namespace MySolution.Module.BusinessObjects {
        public interface IPictureItem {
            byte[] Image { get; }
            string Text { get; }
        }
    }
    
  2. In the MySolution.Module project, create a business class that implements the IPictureItem interface. Name this class PictureItem.

    File: MySolution.Module\BusinessObjects\PictureItem.cs

    using DevExpress.Persistent.Base;
    using DevExpress.Persistent.BaseImpl;
    using DevExpress.Xpo;
    
    namespace MySolution.Module.BusinessObjects {
        [DefaultClassOptions]
        public class PictureItem : BaseObject, IPictureItem {
            private byte[] image;
            private string text;
            public PictureItem(Session session) : base(session) { }
            [ImageEditor]
            public byte[] Image {
                get { return image; }
                set { SetPropertyValue(nameof(Image), ref image, value); }
            }
            public string Text {
                get { return text; }
                set { SetPropertyValue(nameof(Text), ref text, value); }
            }
        }
    }
    
    using DevExpress.Persistent.Base;
    using System.ComponentModel;
    using System.Runtime.CompilerServices;
    
    namespace MySolution.Module.BusinessObjects {
        [DefaultClassOptions]
        public class PictureItem : INotifyPropertyChanged, IPictureItem {
            private byte[] image;
            private string text;
            public int PictureItemId { get; protected set; }
            [ImageEditor]
            public byte[] Image {
                get { return image; }
                set {
                    if (image != value) {
                        image = value;
                        OnPropertyChanged();
                    }
                }
            }
            public string Text {
                get { return text; }
                set {
                    if (text != value) {
                        text = value;
                        OnPropertyChanged();
                    }
                }
            }
            public event PropertyChangedEventHandler PropertyChanged;
            protected void OnPropertyChanged([CallerMemberName] string propertyName = null) {
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }    
    
  3. In EF Core-based applications, register the PictureItem entity in the DbContext:

    File: MySolution.Module\BusinessObjects\MySolutionDbContext.cs

    namespace MySolution.Module.BusinessObjects {
        // ...
        [TypesInfoInitializer(typeof(MySolutionContextInitializer))]
        public class MySolutionEFCoreDbContext : DbContext {
            // ...
            public DbSet<PictureItem> PictureItems { get; set; }
        }
    }
    

Razor Component

  1. Create a new Razor component (PictureItemListView in this example) in the ASP.NET Core Blazor module project (MySolution.Module.Blazor). If your solution does not contain this project, add this component to the application project (MySolution.Blazor.Server).

  2. Ensure that the component’s Build Action property is set to Content.

  3. Declare the Data and ItemClick component parameters.

  4. Iterate through the Data collection and define the required markup for each data object.

  5. Specify the @onclick delegate event handler for a data object. See the following topic for details: ASP.NET Core Blazor event handling.

Note

The PictureItemListView component supports only PNG images.

File:
MySolution.Blazor.Server\PictureItemListView.razor in solutions without the ASP.NET Core Blazor-specific Module project;
MySolution.Module.Blazor\PictureItemListView.razor in solutions with the ASP.NET Core Blazor-specific Module project.

@using Microsoft.AspNetCore.Components.Web
@using MySolution.Module.BusinessObjects

@if (Data is not null) {
    <div class="row">
        @foreach (var item in Data) {
            <div class="col-auto" style="cursor: pointer;"
                 @onclick=@(async () => await ItemClick.InvokeAsync(item))>
                @if (item.Image is null) {
                    <div class="border d-flex justify-content-center align-items-center"
                         style="height:150px; width: 104px;">
                        No image
                    </div>
                }
                else {
                    <img src="data:image/png;base64,@Convert.ToBase64String(item.Image)" alt=@item.Text
                         style="height:150px; width: 104px;">
                }
                <div class="text-center" style="width: 104px;">
                    @item.Text
                </div>
            </div>
        }
    </div>
}

@code {
    [Parameter]
    public IEnumerable<IPictureItem> Data { get; set; }
    [Parameter]
    public EventCallback<IPictureItem> ItemClick { get; set; }
}

Component Model

Create a ComponentModelBase descendant and name it PictureItemListViewModel. In this class, declare properties that describe the component and its interaction with a user.

File:
MySolution.Blazor.Server\PictureItemListViewModel.cs in solutions without the ASP.NET Core Blazor-specific Module project;
MySolution.Module.Blazor\PictureItemListViewModel.cs in solutions with the ASP.NET Core Blazor-specific Module project.

using System;
using System.Collections.Generic;
using DevExpress.ExpressApp.Blazor.Components.Models;
using MySolution.Module.BusinessObjects;

namespace MySolution.Module.Blazor {
    public class PictureItemListViewModel : ComponentModelBase {
        public IEnumerable<IPictureItem> Data {
            get => GetPropertyValue<IEnumerable<IPictureItem>>();
            set => SetPropertyValue(value);
        }
        public void Refresh() => RaiseChanged();
        public void OnItemClick(IPictureItem item) =>
            ItemClick?.Invoke(this, new PictureItemListViewModelItemClickEventArgs(item));
        public event EventHandler<PictureItemListViewModelItemClickEventArgs> ItemClick;
    }
    public class PictureItemListViewModelItemClickEventArgs : EventArgs {
        public PictureItemListViewModelItemClickEventArgs(IPictureItem item) {
            Item = item;
        }
        public IPictureItem Item { get; }
    }
}

Component Renderer

  1. Create a new Razor component and name it PictureItemListViewRenderer.

  2. Ensure that the component’s Build Action property is set to Content.

  3. Add the PictureItemListView Razor component that we created in the Razor Component section to PictureItemListViewRenderer.

  4. Declare the ComponentModel parameter to bind PictureItemListView with its model.

  5. Specify the PictureItemListView.Data and PictureItemListView.ItemClick parameters to map them to the model properties.

  6. Implement the PictureItemListViewRenderer.Create method to create the RenderFragment and add the PictureItemListViewRenderer content to the List View layout.

File:
MySolution.Blazor.Server\PictureItemListViewRenderer.razor in solutions without the ASP.NET Core Blazor-specific Module project;
MySolution.Module.Blazor\PictureItemListViewRenderer.razor in solutions with the ASP.NET Core Blazor-specific Module project.

<PictureItemListView Data=@ComponentModel.Data ItemClick=@ComponentModel.OnItemClick />

@code {
    public static RenderFragment Create(PictureItemListViewModel componentModel) =>
    @<PictureItemListViewRenderer ComponentModel=@componentModel />;
[Parameter]
public PictureItemListViewModel ComponentModel { get; set; }
}

List Editor

Tip

You can find the full List Editor file code at the end of this topic: BlazorCustomListEditor.cs.

  1. Create a ListEditor descendant and name it BlazorCustomListEditor.

  2. Apply the following ListEditorAttribute to the BlazorCustomListEditor class: [ListEditor(typeof(IPictureItem))]. This attribute value makes BlazorCustomListEditor the default editor for any IPictureItem List View.

  3. Create a nested class that implements the IComponentContentHolder interface and name it PictureItemListViewHolder. Use the PictureItemListViewRenderer.Create method implemented in the Component Renderer section to create a RenderFragment with the List Editor layout.

    File:
    MySolution.Blazor.Server\BlazorCustomListEditor.cs in solutions without the ASP.NET Core Blazor-specific Module project;
    MySolution.Module.Blazor\BlazorCustomListEditor.cs in solutions with the ASP.NET Core Blazor-specific Module project.

    using System;
    using System.Collections;
    using System.ComponentModel;
    using System.Linq;
    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.Blazor;
    using DevExpress.ExpressApp.Blazor.Components;
    using DevExpress.ExpressApp.Editors;
    using DevExpress.ExpressApp.Model;
    using Microsoft.AspNetCore.Components;
    using MySolution.Module.BusinessObjects;
    
    namespace MySolution.Module.Blazor {
        [ListEditor(typeof(IPictureItem))]
        public class BlazorCustomListEditor : ListEditor {
            public class PictureItemListViewHolder : IComponentContentHolder {
                private RenderFragment componentContent;
                public PictureItemListViewHolder(PictureItemListViewModel componentModel) {
                    ComponentModel =
                        componentModel ?? throw new ArgumentNullException(nameof(componentModel));
                }
                private RenderFragment CreateComponent() =>
                    ComponentModelObserver.Create(ComponentModel,
                                                    PictureItemListViewRenderer.Create(ComponentModel));
                public PictureItemListViewModel ComponentModel { get; }
                RenderFragment IComponentContentHolder.ComponentContent =>
                    componentContent ??= CreateComponent();
            }
            // ...
            public BlazorCustomListEditor(IModelListView model) : base(model) { }
            // ...
        }
        // ...
    }
    
  4. Override the CreateControlsCore method to return a PictureItemListViewHolder instance. Note that in the XAF Blazor application, CreateControlsCore should return an instance that implements the IComponentContentHolder interface.

    [ListEditor(typeof(IPictureItem))]
    // ...
    public class BlazorCustomListEditor : ListEditor {
    // ...
        protected override object CreateControlsCore() =>
        // ...
            new PictureItemListViewHolder(new PictureItemListViewModel());
            // ...
    }
    
  5. Override the AssignDataSourceToControl method. In this method, assign the List Editor’s data source to the component model. If the data source implements the IBindingList interface, handle data change notifications.

    [ListEditor(typeof(IPictureItem))]
    // ...
    public class BlazorCustomListEditor : ListEditor {
    // ...
        protected override void AssignDataSourceToControl(object dataSource) {
            if (Control is PictureItemListViewHolder holder) {
                if (holder.ComponentModel.Data is IBindingList bindingList) {
                    bindingList.ListChanged -= BindingList_ListChanged;
                }
                holder.ComponentModel.Data =
                    (dataSource as IEnumerable)?.OfType<IPictureItem>().OrderBy(i => i.Text);
                if (dataSource is IBindingList newBindingList) {
                    newBindingList.ListChanged += BindingList_ListChanged;
                }
            }
        }
        // ...
        private void BindingList_ListChanged(object sender, ListChangedEventArgs e) {
            Refresh();
        }
        // ...
    }
    
  6. Override the OnControlsCreated method. In this method, subscribe to the component model’s ItemClick event. In the ComponentModel_ItemClick event handler, set the selectedObjects field to the clicked item, and call the OnSelectionChanged and OnProcessSelectedItem methods to handle an item’s selection.

    [ListEditor(typeof(IPictureItem))]
    // ...
    public class BlazorCustomListEditor : ListEditor {
    // ...
        private IPictureItem[] selectedObjects = Array.Empty<IPictureItem>();
        // ...
        protected override void OnControlsCreated() {
            if (Control is PictureItemListViewHolder holder) {
                holder.ComponentModel.ItemClick += ComponentModel_ItemClick;
            }
            base.OnControlsCreated();
        }
        // ...
        private void ComponentModel_ItemClick(object sender,
                                                PictureItemListViewModelItemClickEventArgs e) {
            selectedObjects = new IPictureItem[] { e.Item };
            OnSelectionChanged();
            OnProcessSelectedItem();
        }
        // ...
    }
    
  7. Override the BreakLinksToControls() method. In this method, unsubscribe from the component model’s events and reset its data to release resources. Override the Refresh() method. In this method, call the PictureItemListViewModel.Refresh method to update the ListEditor layout when its data is changed.

    [ListEditor(typeof(IPictureItem))]
    // ...
    public class BlazorCustomListEditor : ListEditor {
    // ...
        public override void BreakLinksToControls() {
            if (Control is PictureItemListViewHolder holder) {
                holder.ComponentModel.ItemClick -= ComponentModel_ItemClick;
            }
            AssignDataSourceToControl(null);
            base.BreakLinksToControls();
        }
        // ...
        public override void Refresh() {
            if (Control is PictureItemListViewHolder holder) {
                holder.ComponentModel.Refresh();
            }
        }
        // ...
    }
    
  8. Override the SelectionType property to return SelectionType.Full. This setting allows a user to open the Detail View by click.

    [ListEditor(typeof(IPictureItem))]
    // ...
    public class BlazorCustomListEditor : ListEditor {
    // ...
        public override SelectionType SelectionType => SelectionType.Full;
        // ...
    }
    
  9. Override the GetSelectedObjects() method. In this method, return the selectedObjects field value.

    [ListEditor(typeof(IPictureItem))]
    // ...
    public class BlazorCustomListEditor : ListEditor {
    // ...
        public override IList GetSelectedObjects() => selectedObjects;
        // ...
    }
    

The full BlazorCustomListEditor.cs file code:

using System;
using System.Collections;
using System.ComponentModel;
using System.Linq;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Blazor;
using DevExpress.ExpressApp.Blazor.Components;
using DevExpress.ExpressApp.Editors;
using DevExpress.ExpressApp.Model;
using Microsoft.AspNetCore.Components;
using MySolution.Module.BusinessObjects;

namespace MySolution.Module.Blazor {
    [ListEditor(typeof(IPictureItem))]
    public class BlazorCustomListEditor : ListEditor {
        public class PictureItemListViewHolder : IComponentContentHolder {
            private RenderFragment componentContent;
            public PictureItemListViewHolder(PictureItemListViewModel componentModel) {
                ComponentModel =
                    componentModel ?? throw new ArgumentNullException(nameof(componentModel));
            }
            private RenderFragment CreateComponent() =>
                ComponentModelObserver.Create(ComponentModel,
                                                PictureItemListViewRenderer.Create(ComponentModel));
            public PictureItemListViewModel ComponentModel { get; }
            RenderFragment IComponentContentHolder.ComponentContent =>
                componentContent ??= CreateComponent();
        }
        private IPictureItem[] selectedObjects = Array.Empty<IPictureItem>();
        public BlazorCustomListEditor(IModelListView model) : base(model) { }
        protected override object CreateControlsCore() =>
            new PictureItemListViewHolder(new PictureItemListViewModel());
        protected override void AssignDataSourceToControl(object dataSource) {
            if (Control is PictureItemListViewHolder holder) {
                if (holder.ComponentModel.Data is IBindingList bindingList) {
                    bindingList.ListChanged -= BindingList_ListChanged;
                }
                holder.ComponentModel.Data =
                    (dataSource as IEnumerable)?.OfType<IPictureItem>().OrderBy(i => i.Text);
                if (dataSource is IBindingList newBindingList) {
                    newBindingList.ListChanged += BindingList_ListChanged;
                }
            }
        }
        protected override void OnControlsCreated() {
            if (Control is PictureItemListViewHolder holder) {
                holder.ComponentModel.ItemClick += ComponentModel_ItemClick;
            }
            base.OnControlsCreated();
        }
        public override void BreakLinksToControls() {
            if (Control is PictureItemListViewHolder holder) {
                holder.ComponentModel.ItemClick -= ComponentModel_ItemClick;
            }
            AssignDataSourceToControl(null);
            base.BreakLinksToControls();
        }
        public override void Refresh() {
            if (Control is PictureItemListViewHolder holder) {
                holder.ComponentModel.Refresh();
            }
        }
        private void BindingList_ListChanged(object sender, ListChangedEventArgs e) {
            Refresh();
        }
        private void ComponentModel_ItemClick(object sender,
                                                PictureItemListViewModelItemClickEventArgs e) {
            selectedObjects = new IPictureItem[] { e.Item };
            OnSelectionChanged();
            OnProcessSelectedItem();
        }
        public override SelectionType SelectionType => SelectionType.Full;
        public override IList GetSelectedObjects() => selectedObjects;
    }
}

The custom List Editor supports only the Client data access mode. Set the Client data access mode in the static DataAccessModeHelper.RegisterEditorSupportedModes method as described in the Specify Data Access Mode section of the following topic: List View Data Access Modes.

File:
MySolution.Blazor.Server\BlazorModule.cs in solutions without the ASP.NET Core Blazor-specific Module project;
MySolution.Module.Blazor\BlazorModule.cs in solutions with the ASP.NET Core Blazor-specific Module project.

using DevExpress.ExpressApp.Utils;
// ...
    public sealed partial class MySolutionBlazorModule : ModuleBase {
        public MySolutionBlazorModule() {
            InitializeComponent();
            DataAccessModeHelper.RegisterEditorSupportedModes(typeof(BlazorCustomListEditor),
                                                        new[] { CollectionSourceDataAccessMode.Client });
        }
        // ...
    }

Access XafApplication and ObjectSpace to Query and Manipulate Data (Perform CRUD Operations)

A custom List Editor may require access to the application object or the List View Collection Source (the List View data source). If so, implement the IComplexListEditor interface as shown in the following topic: IComplexListEditor.

Use the IComplexListEditor.Setup method to get the XafApplication and CollectionSourceBase objects. The CollectionSourceBase class is the base class for Collection Source classes, which allow you to manipulate the ObjectSpace data.

See Also