skills/kashiash/xaf-skills/xaf-custom-editors

xaf-custom-editors

SKILL.md

XAF: Custom Property Editors & List Editors

When to Build a Custom Editor

  • Wrap a third-party control not natively supported by XAF
  • Display data in a specialized way (color picker, rating stars, progress ring)
  • Combine multiple properties into one visual component
  • Use a platform-specific control (Blazor component library, WinForms control)

Blazor Custom Property Editor

Base Class

Inherit from BlazorPropertyEditorBase (namespace: DevExpress.ExpressApp.Blazor.Editors).

using DevExpress.ExpressApp.Blazor.Editors;
using DevExpress.ExpressApp.Model;
using DevExpress.ExpressApp.Editors;

[PropertyEditor(typeof(int), "RatingPropertyEditor", false)]
public class RatingPropertyEditor : BlazorPropertyEditorBase {
    public RatingPropertyEditor(Type objectType, IModelMemberViewItem model)
        : base(objectType, model) { }

    protected override IComponentModel CreateComponentAdapter() {
        return new RatingComponentModel();
    }

    protected override IComponentModel CreateViewerComponentAdapter() {
        return new RatingComponentModel { ReadOnly = true };
    }

    protected override object GetControlValueCore() {
        return ((RatingComponentModel)ComponentModel).Value;
    }

    protected override void ReadValueToControl(object value) {
        if (ComponentModel is RatingComponentModel model)
            model.Value = value is int v ? v : 0;
    }
}

ComponentModel (Blazor bridge)

using DevExpress.ExpressApp.Blazor.Editors.Adapters;

public class RatingComponentModel : ComponentModelBase {
    private int value;
    private bool readOnly;

    public int Value {
        get => value;
        set {
            SetProperty(ref this.value, value);
            ValueChanged?.Invoke(this, EventArgs.Empty);
        }
    }

    public bool ReadOnly {
        get => readOnly;
        set => SetProperty(ref readOnly, value);
    }

    public event EventHandler ValueChanged;
    public override Type ComponentType => typeof(RatingEditorComponent);
}

Razor Component

@* RatingEditorComponent.razor *@
<div class="rating-editor">
    @for (int i = 1; i <= 5; i++) {
        int star = i;
        <span class="@(star <= Model.Value ? "star filled" : "star")"
              @onclick="() => OnStarClick(star)"></span>
    }
</div>

@code {
    [Parameter]
    public RatingComponentModel Model { get; set; }

    private void OnStarClick(int star) {
        if (!Model.ReadOnly) Model.Value = star;
    }
}

Registration via [PropertyEditor] Attribute

// [PropertyEditor(typeof(PropertyType), "AliasString", isDefault)]
[PropertyEditor(typeof(int), "RatingPropertyEditor", false)]
// isDefault: true  → auto-applied to ALL int properties
// isDefault: false → must assign via [EditorAlias] or Application Model

Assign to a specific property:

[EditorAlias("RatingPropertyEditor")]
public virtual int StarRating { get; set; }

WinForms Custom Property Editor

WinPropertyEditor (standard WinForms controls)

using DevExpress.ExpressApp.Win.Editors;
using DevExpress.ExpressApp.Editors;
using DevExpress.ExpressApp.Model;

[PropertyEditor(typeof(int), "TrackBarPropertyEditor", false)]
public class TrackBarPropertyEditor : WinPropertyEditor {
    public TrackBarPropertyEditor(Type objectType, IModelMemberViewItem model)
        : base(objectType, model) { }

    protected override object CreateControlCore() {
        var trackBar = new TrackBar {
            Minimum = 0,
            Maximum = 100,
            TickFrequency = 10
        };
        trackBar.Scroll += TrackBar_Scroll;
        ControlBindingProperty = nameof(TrackBar.Value); // auto data binding
        return trackBar;
    }

    private void TrackBar_Scroll(object sender, EventArgs e) {
        OnControlValueChanged(); // notify XAF that value changed
    }

    protected override void BreakLinksToControl(bool unwireEventsOnly) {
        if (Control is TrackBar trackBar)
            trackBar.Scroll -= TrackBar_Scroll;
        base.BreakLinksToControl(unwireEventsOnly);
    }

    public new TrackBar Control => (TrackBar)base.Control;
}

DXPropertyEditor (DevExpress controls with RepositoryItem)

using DevExpress.XtraEditors;
using DevExpress.XtraEditors.Repository;
using DevExpress.ExpressApp.Win.Editors;

[PropertyEditor(typeof(int), "SpinEditPropertyEditor", false)]
public class SpinEditPropertyEditor : DXPropertyEditor {
    public SpinEditPropertyEditor(Type objectType, IModelMemberViewItem model)
        : base(objectType, model) { }

    protected override void SetupRepositoryItem(RepositoryItem item) {
        base.SetupRepositoryItem(item);
        if (item is RepositoryItemSpinEdit spinItem) {
            spinItem.MinValue = 0;
            spinItem.MaxValue = 999;
            spinItem.IsFloatValue = false;
        }
    }

    protected override object CreateControlCore() {
        return new SpinEdit();
    }
}

Required Override Summary (WinForms)

Member Purpose
CreateControlCore() Instantiate and configure control; return it
OnControlValueChanged() Call from control's change event
ControlBindingProperty Names control property for auto data binding
BreakLinksToControl(bool) Unsubscribe events; base handles disposal

Custom List Editor

using DevExpress.ExpressApp.Editors;
using DevExpress.ExpressApp.Model;

[ListEditor(typeof(object), isDefault: false)]
public class CardListEditor : ListEditor {
    private CardListControl control;

    public CardListEditor(IModelListView info) : base(info) { }

    protected override object CreateControlsCore() {
        control = new CardListControl();
        control.SelectionChanged += (s, e) => OnSelectionChanged();
        control.ItemDoubleClick += (s, e) => OnProcessSelectedItem();
        AssignDataSourceToControl(DataSource);
        return control;
    }

    protected override void AssignDataSourceToControl(IEnumerable dataSource) {
        if (control != null)
            control.DataSource = dataSource;
    }

    public override void Refresh() => control?.Refresh();

    public override IList GetSelectedObjects() =>
        control?.SelectedItems ?? new List<object>();

    public override SelectionType SelectionType => SelectionType.MultipleSelection;

    public override string[] RequiredProperties => Array.Empty<string>();

    public override object FocusedObject {
        get => control?.FocusedItem;
        set { if (control != null) control.FocusedItem = value; }
    }

    public override void Dispose() {
        control?.Dispose();
        control = null;
        base.Dispose();
    }
}

Key ListEditor Members

Member Required Purpose
CreateControlsCore() Yes Create and return the list control
AssignDataSourceToControl(IEnumerable) Yes Bind data to control
Refresh() Yes Reload/repaint control data
GetSelectedObjects() Yes Return currently selected objects
SelectionType Yes None, SingleObject, MultipleSelection
RequiredProperties Yes Property names the control needs
FocusedObject Yes Get/set the focused (current) object
OnSelectionChanged() Call when selection changes Fires SelectionChanged event
OnProcessSelectedItem() Call on double-click Opens Detail View

Manual Registration via EditorDescriptorsFactory

Override in your module (preferred for large apps — avoids attribute scanning):

protected override void RegisterEditorDescriptors(EditorDescriptorsFactory factory) {
    base.RegisterEditorDescriptors(factory);

    factory.RegisterPropertyEditor(
        "RatingPropertyEditor",
        typeof(int),
        typeof(RatingPropertyEditor),
        isDefaultEditor: false);

    factory.RegisterListEditor(
        typeof(MyObject),
        typeof(CardListEditor),
        isDefaultEditor: true);
}

Accessing Editors in Controllers

// Property editor in DetailView
public class MyDetailViewController : ObjectViewController<DetailView, MyObject> {
    protected override void OnViewControlsCreated() {
        base.OnViewControlsCreated();
        var editor = View.FindItem("StarRating") as RatingPropertyEditor;
        if (editor?.ComponentModel is RatingComponentModel model) {
            // customize
        }
    }
}

// List editor in ListView
public class MyListViewController : ObjectViewController<ListView, MyObject> {
    protected override void OnViewControlsCreated() {
        base.OnViewControlsCreated();
        if (View.Editor is CardListEditor listEditor) {
            // access custom list editor members
        }
    }
}

v24.2 vs v25.1 Notes

No breaking changes to the custom editor API between v24.2 and v25.1.

  • Assembly version suffix changes (v24.2.dllv25.1.dll)
  • DxGridListEditor in Blazor: enhanced column API in v25.1
  • .NET target: v24.2 supports .NET 8; v25.1 supports .NET 8 and .NET 9

Source Links

Weekly Installs
4
GitHub Stars
2
First Seen
3 days ago
Installed on
opencode4
gemini-cli4
claude-code4
github-copilot4
codex4
amp4