How to: Use a Custom Component to Implement List Editor (Blazor)
- 7 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.
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 application project (YourSolutionName.Blazor.Server).
- Razor Component - to define the required markup;
- Component Model - to change the state of the component;
- List Editor - to integrate the component into your XAF application.
Define the Data Model
In the CustomEditorEF.Module project, create a new interface and name it IPictureItem. In this interface, declare the
Image
andText
properties. This allows the List Editor to work with different types of objects that implement this interface.File: CustomEditorEF.Module\BusinessObjects\IPictureItem.cs
namespace CustomEditorEF.Module.BusinessObjects; public interface IPictureItem { byte[] Image { get; } string Text { get; } }
In the CustomEditorEF.Module project, create a business class that implements the
IPictureItem
interface. Name this classPictureItem
.File: CustomEditorEF.Module\BusinessObjects\PictureItem.cs
using DevExpress.Persistent.Base; using DevExpress.Persistent.BaseImpl; using DevExpress.Persistent.BaseImpl.EF; namespace CustomEditorEF.Module.BusinessObjects; [DefaultClassOptions] public class PictureItem : BaseObject, IPictureItem { [ImageEditor] public virtual byte[] Image { get; set; } public virtual string Text { get; set; } }
Register the
PictureItems
entity in theDbContext
:File: CustomEditorEF.Module\BusinessObjects\CustomEditorEFDbContext.cs
namespace CustomEditorEF.Module.BusinessObjects; // ... public class CustomEditorEFContextInitializer : DbContextTypesInfoInitializerBase { // ... [TypesInfoInitializer(typeof(CustomEditorEFContextInitializer))] public class CustomEditorEFEFCoreDbContext : DbContext { public CustomEditorEFEFCoreDbContext(DbContextOptions<CustomEditorEFEFCoreDbContext> options) : base(options) { } public DbSet<PictureItem> PictureItems { get; set; } // ... } }
Razor Component
Create a new Razor component (
PictureItemListView
in this example) in the ASP.NET Core Blazor application project (CustomEditorEF.Blazor.Server).Ensure that the component’s
Build Action
property is set toContent
.Declare the
Data
andItemClick
component parameters.Iterate through the
Data
collection and define the markup for each data object.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: CustomEditorEF.Blazor.Server\Editors\CustomList\PictureItemListView.razor
@using CustomEditorEF.Module.BusinessObjects;
@using Microsoft.AspNetCore.Components.Web
@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 with the following name: PictureItemListViewModel
. In this class, declare properties that match the component above.
File: CustomEditorEF.Blazor.Server\Editors\CustomList\PictureItemListViewModel.cs
using DevExpress.ExpressApp.Blazor.Components.Models;
using CustomEditorEF.Module.BusinessObjects;
using Microsoft.AspNetCore.Components;
namespace CustomEditorEF.Blazor.Server.Editors.CustomList;
public class PictureItemListViewModel : ComponentModelBase {
public IEnumerable<IPictureItem> Data {
get => GetPropertyValue<IEnumerable<IPictureItem>>();
set => SetPropertyValue(value);
}
public EventCallback<IPictureItem> ItemClick {
get => GetPropertyValue<EventCallback<IPictureItem>>();
set => SetPropertyValue(value);
}
public override Type ComponentType => typeof(PictureItemListView);
}
List Editor
Tip
You can find the full List Editor file code at the end of this topic: BlazorCustomListEditor.cs.
Create a ListEditor descendant with the following name:
BlazorCustomListEditor
.Apply the following ListEditorAttribute to the
BlazorCustomListEditor
class:[ListEditor(typeof(IPictureItem))]
. This attribute value makesBlazorCustomListEditor
the default editor for anyIPictureItem
List View.Implement the
IComponentContentHolder
interface to create a RenderFragment with the List Editor layout.File: CustomEditorEF.Blazor.Server\Editors\CustomList\BlazorCustomListEditor.cs
using System; using System.Collections; using System.ComponentModel; using System.Linq; using CustomEditorEF.Module.BusinessObjects; using DevExpress.ExpressApp; using DevExpress.ExpressApp.Blazor; using DevExpress.ExpressApp.Blazor.Components; using DevExpress.ExpressApp.Editors; using DevExpress.ExpressApp.Model; using Microsoft.AspNetCore.Components; namespace CustomEditorEF.Blazor.Server.Editors.CustomList; [ListEditor(typeof(IPictureItem))] public class BlazorCustomListEditor : ListEditor, IComponentContentHolder { private RenderFragment _componentContent; public PictureItemListViewModel ComponentModel { get; private set; } public RenderFragment ComponentContent { get { _componentContent ??= ComponentModelObserver.Create(ComponentModel, ComponentModel.GetComponentContent()); return _componentContent; } } public BlazorCustomListEditor(IModelListView model) : base(model) { } // ... }
Override the
CreateControlsCore
method to return aPictureItemListViewModel
instance. Handle theItemClick
event to process the selected item.[ListEditor(typeof(IPictureItem))] public class BlazorCustomListEditor : ListEditor { private IPictureItem[] selectedObjects = Array.Empty<IPictureItem>(); public PictureItemListViewModel ComponentModel { get; private set; } // ... protected override object CreateControlsCore() { ComponentModel = new PictureItemListViewModel(); ComponentModel.ItemClick = EventCallback.Factory.Create<IPictureItem>(this, (item) => { selectedObjects = new IPictureItem[] { item }; OnSelectionChanged(); OnProcessSelectedItem(); }); return ComponentModel; } }
Override the
AssignDataSourceToControl
method. In this method, assign the List Editor’s data source to the component model. If the data source implements theIBindingList
interface, handle data change notifications.[ListEditor(typeof(IPictureItem))] public class BlazorCustomListEditor : ListEditor { // ... protected override void AssignDataSourceToControl(object dataSource) { if(ComponentModel is not null) { if(ComponentModel.Data is IBindingList bindingList) { bindingList.ListChanged -= BindingList_ListChanged; } UpdateDataSource(dataSource); if(dataSource is IBindingList newBindingList) { newBindingList.ListChanged += BindingList_ListChanged; } } } private void BindingList_ListChanged(object sender, ListChangedEventArgs e) { UpdateDataSource(DataSource); } private void UpdateDataSource(object dataSource) { if(ComponentModel is not null) { ComponentModel.Data = (dataSource as IEnumerable)?.OfType<IPictureItem>().OrderBy(i => i.Text); } } }
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 updateListEditor
layout when its data is changed.[ListEditor(typeof(IPictureItem))] public class BlazorCustomListEditor : ListEditor { // ... public override void BreakLinksToControls() { AssignDataSourceToControl(null); base.BreakLinksToControls(); } // ... public override void Refresh() => UpdateDataSource(DataSource); // ... }
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; // ... }
Override the GetSelectedObjects() method. In this method, return the
selectedObjects
field value.[ListEditor(typeof(IPictureItem))] public class BlazorCustomListEditor : ListEditor { // ... public override IList GetSelectedObjects() => selectedObjects; }
Full List Editor Code
The full BlazorCustomListEditor.cs file code:
using System.Collections;
using System.ComponentModel;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Blazor;
using DevExpress.ExpressApp.Blazor.Components;
using DevExpress.ExpressApp.Blazor.Components.Models;
using DevExpress.ExpressApp.Editors;
using DevExpress.ExpressApp.Model;
using CustomEditorEF.Module.BusinessObjects;
using Microsoft.AspNetCore.Components;
namespace CustomEditorEF.Blazor.Server.Editors.CustomList;
[ListEditor(typeof(IPictureItem))]
public class BlazorCustomListEditor : ListEditor, IComponentContentHolder {
private RenderFragment _componentContent;
private IPictureItem[] selectedObjects = Array.Empty<IPictureItem>();
public PictureItemListViewModel ComponentModel { get; private set; }
public RenderFragment ComponentContent {
get {
_componentContent ??= ComponentModelObserver.Create(ComponentModel, ComponentModel.GetComponentContent());
return _componentContent;
}
}
public BlazorCustomListEditor(IModelListView model) : base(model) { }
private void BindingList_ListChanged(object sender, ListChangedEventArgs e) {
UpdateDataSource(DataSource);
}
private void UpdateDataSource(object dataSource) {
if(ComponentModel is not null) {
ComponentModel.Data = (dataSource as IEnumerable)?.OfType<IPictureItem>().OrderBy(i => i.Text);
}
}
protected override object CreateControlsCore() {
ComponentModel = new PictureItemListViewModel();
ComponentModel.ItemClick = EventCallback.Factory.Create<IPictureItem>(this, (item) => {
selectedObjects = new IPictureItem[] { item };
OnSelectionChanged();
OnProcessSelectedItem();
});
return ComponentModel;
}
protected override void AssignDataSourceToControl(object dataSource) {
if(ComponentModel is not null) {
if(ComponentModel.Data is IBindingList bindingList) {
bindingList.ListChanged -= BindingList_ListChanged;
}
UpdateDataSource(dataSource);
if(dataSource is IBindingList newBindingList) {
newBindingList.ListChanged += BindingList_ListChanged;
}
}
}
public override void BreakLinksToControls() {
AssignDataSourceToControl(null);
base.BreakLinksToControls();
}
public override void Refresh() => UpdateDataSource(DataSource);
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: CustomEditorEF.Blazor.Server\BlazorModule.cs
using DevExpress.ExpressApp.Utils;
// ...
public sealed class CustomEditorEFBlazorModule : ModuleBase {
public CustomEditorEFBlazorModule() {
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 that allow you to manipulate ObjectSpace data.