Skip to main content
All docs
V24.2
.NET 8.0+

How To: Create a Custom Blazor Application Template

  • 8 minutes to read

This article explains how to create and use a custom Main Form Template instead of the Application Window Template in an XAF ASP.NET Core Blazor application.

XAF applications created in the Solution Wizard use a DxAccordion or DxTreeView navigation component. The custom template described in this topic uses the DxMenu navigation component.

View Example: XAF Blazor - How to create a custom template

Implementation Details

  1. In the Solution Explorer, navigate to the YourSolutionName.Blazor.Server project and create the Templates folder.
  2. Create a custom Action control that displays a custom navigation component. Right-click the Templates folder, select the Add | Class… option from the context menu, and set the name of the new class to CustomShowNavigationItemActionControl.cs. Implement the ISingleChoiceActionControl interface in the newly created class. Set the wrapped Action’s ActionId property to ShowNavigationItem.

    using DevExpress.ExpressApp.Actions;
    using DevExpress.ExpressApp.Templates;
    using DevExpress.ExpressApp.Templates.ActionControls;
    using Microsoft.AspNetCore.Components;
    
    namespace YourSolutionName.Blazor.Server.Templates;
    
    public class CustomShowNavigationItemActionControl : ISingleChoiceActionControl {
        private ChoiceActionItemCollection choiceActionItems;
        private EventHandler<SingleChoiceActionControlExecuteEventArgs> execute;
        string IActionControl.ActionId => "ShowNavigationItem";
        object IActionControl.NativeControl => this;
        public IEnumerable<ChoiceActionItem> Items => choiceActionItems;
        // The CustomShowNavigationItemActionControlComponent is added in the next step.
        public RenderFragment GetComponentContent(RenderFragment titleTemplate) => CustomShowNavigationItemActionControlComponent.Create(titleTemplate, this);
        void ISingleChoiceActionControl.SetChoiceActionItems(ChoiceActionItemCollection choiceActionItems) => this.choiceActionItems = choiceActionItems;
        public void DoExecute(ChoiceActionItem choiceActionItem) {
            execute?.Invoke(this, choiceActionItem == null ? new SingleChoiceActionControlExecuteEventArgs() : new SingleChoiceActionControlExecuteEventArgs(choiceActionItem));
        }
        event EventHandler<SingleChoiceActionControlExecuteEventArgs> ISingleChoiceActionControl.Execute {
            add => execute += value;
            remove => execute -= value;
        }
    
        void IActionControl.SetCaption(string caption) { }
        void IActionControl.SetConfirmationMessage(string confirmationMessage) { }
        void IActionControl.SetEnabled(bool enabled) { }
        void IActionControl.SetImage(string imageName) { }
        void IActionControl.SetPaintStyle(ActionItemPaintStyle paintStyle) { }
        void ISingleChoiceActionControl.SetSelectedItem(ChoiceActionItem selectedItem) { }
        void IActionControl.SetShortcut(string shortcutString) { }
        void ISingleChoiceActionControl.SetShowItemsOnClick(bool value) { }
        void IActionControl.SetToolTip(string toolTip) { }
        void ISingleChoiceActionControl.Update(IDictionary<object, ChoiceActionItemChangesType> itemsChangedInfo) { }
        void IActionControl.SetVisible(bool visible) { }
        event EventHandler IActionControl.NativeControlDisposed { add { } remove { } }
    }
    
  3. Create a custom Razor component that renders the DxMenu component. Right-click the Templates folder, select the Add | Razor Component option from the context menu, and set the new component’s name to CustomShowNavigationItemActionControlComponent.razor. Replace the autogenerated file content with the following code:

    @* File: CustomShowNavigationItemActionControlComponent.razor *@
    @using DevExpress.ExpressApp.Actions
    
    <DxMenu Data="@ActionControl.Items" ItemClick="@OnItemClick" HamburgerButtonPosition="MenuHamburgerButtonPosition.Left" CollapseItemsToHamburgerMenu="true">
        <TitleTemplate>
            @TitleTemplate
        </TitleTemplate>
        <DataMappings>
            <DxMenuDataMapping Text="@nameof(ChoiceActionItem.Caption)" Children="@nameof(ChoiceActionItem.Items)" />
        </DataMappings>
    </DxMenu>
    
    @code {
        public static RenderFragment Create(RenderFragment titleTemplate, CustomShowNavigationItemActionControl actionControl) =>
        @<CustomShowNavigationItemActionControlComponent TitleTemplate="@titleTemplate" ActionControl="@actionControl" />;
        [Parameter]
        public RenderFragment TitleTemplate { get; set; }
        [Parameter]
        public CustomShowNavigationItemActionControl ActionControl { get; set; }
        private void OnItemClick(MenuItemClickEventArgs e) => ActionControl.DoExecute((ChoiceActionItem)e.ItemInfo.Data);
    }
    
  4. Create a new Main Form Template. Right-click the Templates folder and select the Add DevExpress Item | New Item… option from the context menu. In the invoked Template Gallery, navigate to the XAF ASP.NET Core Blazor Templates section and select the Main Form Template item. Name it CustomMainFormTemplate.cs and click Add Item.

  5. In the CustomMainFormTemplate class, replace the built-in ShowNavigationItemActionControl with the newly created CustomShowNavigationItemActionControl:

    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using DevExpress.Blazor;
    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.Blazor.Components.Models;
    using DevExpress.ExpressApp.Blazor.Editors.ActionControls;
    using DevExpress.ExpressApp.Blazor.SystemModule;
    using DevExpress.ExpressApp.Blazor.Templates;
    using DevExpress.ExpressApp.Blazor.Templates.ContextMenu.ActionControls;
    using DevExpress.ExpressApp.Blazor.Templates.Navigation.ActionControls;
    using DevExpress.ExpressApp.Blazor.Templates.Security.ActionControls;
    using DevExpress.ExpressApp.Blazor.Templates.Toolbar.ActionControls;
    using DevExpress.ExpressApp.Templates;
    using DevExpress.ExpressApp.Templates.ActionControls;
    using DevExpress.Persistent.Base;
    using Microsoft.AspNetCore.Components;
    
    namespace YourSolution.Blazor.Server.Templates;
    public class CustomMainFormTemplate : WindowTemplateBase, IMainFormTemplate, ISupportActionsToolbarVisibility, ISelectionDependencyToolbar, ISupportListEditorInlineActions, ISupportListEditorContextMenuActions, ITemplateToolbarProvider, ITabbedMdiMainFormTemplate {
        public CustomMainFormTemplate() : this(null) { }
        public CustomMainFormTemplate(IModelOptionsBlazor modelOptions) {
            ModelOptionsBlazor = modelOptions;
    
            NavigateBackActionControl = new NavigateBackActionControl();
            AddActionControl(NavigateBackActionControl);
            AccountComponent = new AccountComponentAdapter();
            AddActionControls(AccountComponent.ActionControls);
            CustomShowNavigationItemActionControl = new CustomShowNavigationItemActionControl();
            AddActionControl(CustomShowNavigationItemActionControl);
    
            HeaderToolbar = new DxToolbarAdapter(new DxToolbarModel() {
                CssClass = "ps-2"
            });
            HeaderToolbar.AddActionContainer(nameof(PredefinedCategory.QuickAccess), ToolbarItemAlignment.Right);
            HeaderToolbar.AddActionContainer(nameof(PredefinedCategory.Notifications), ToolbarItemAlignment.Right);
            HeaderToolbar.AddActionContainer(nameof(PredefinedCategory.Diagnostic), ToolbarItemAlignment.Right);
    
            if (IsTabbedMdi) {
                ChildTemplates = new ObservableCollection<ITabbedMdiDetailFormTemplate>();
                TabsModel = new DxTabsModel();
                TabsModel.TabsPosition = ModelOptionsBlazor?.TabPosition ?? TabsPosition.Top;
                TabsModel.RenderMode = TabsRenderMode.OnDemand;
                TabsModel.ScrollMode = TabsScrollMode.NavButtons;
                TabsModel.ActiveTabIndex = ActiveTemplateIndex;
                TabsModel.ActiveTabIndexChanged = EventCallback.Factory.Create<int>(this, OnActiveTabIndexChanged);
                TabsModel.CssClass = "xaf-tabbed-mdi h-100";
                TabsModel.ChildContent = builder => {
                    foreach (var item in ChildTemplates) {
                        item.IsActive = item == ActiveTemplate;
                        builder.AddContent(0, item.TabPageModel.GetComponentContent());
                    }
                };
            }
            else {
                IsActionsToolbarVisible = true;
                Toolbar = new DxToolbarAdapter(new DxToolbarModel());
                Toolbar.AddActionContainer(nameof(PredefinedCategory.ObjectsCreation));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.RecordsNavigation), ToolbarItemAlignment.Right);
                Toolbar.AddActionContainer(nameof(PredefinedCategory.SaveOptions), ToolbarItemAlignment.Right, isDropDown: true, defaultActionId: "SaveAndNew", autoChangeDefaultAction: true);
                Toolbar.AddActionContainer(nameof(PredefinedCategory.Save), ToolbarItemAlignment.Right);
                Toolbar.AddActionContainer(nameof(PredefinedCategory.Close));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.UndoRedo));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.Edit));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.RecordEdit));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.View));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.Reports));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.Search));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.Filters));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.FullTextSearch));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.Tools));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.Export));
                Toolbar.AddActionContainer(nameof(PredefinedCategory.Unspecified));
    
                ListEditorActionColumnAdapter = new ListEditorActionColumnAdapter();
                ListEditorActionColumnAdapter.AddActionContainer(nameof(PredefinedCategory.ListView));
                ListEditorActionColumnAdapter.AddActionContainer(nameof(PredefinedCategory.Edit));
                ListEditorActionColumnAdapter.AddActionContainer(nameof(PredefinedCategory.RecordEdit));
    
                DxContextMenuAdapter = new DxContextMenuAdapter(new DxContextMenuModel());
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.ObjectsCreation));
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.Save));
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.SaveOptions), isDropDown: true, defaultActionId: "SaveAndNew", autoChangeDefaultAction: true);
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.Edit));
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.RecordEdit));
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.UndoRedo));
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.Print));
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.View));
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.Export));
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.Reports));
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.OpenObject));
                DxContextMenuAdapter.AddActionContainer(nameof(PredefinedCategory.Menu));
            }
        }
        protected IModelOptionsBlazor ModelOptionsBlazor { get; set; }
        protected override IEnumerable<IActionControlContainer> GetActionControlContainers() {
            if (IsTabbedMdi) {
                return HeaderToolbar.ActionContainers;
            }
            return Toolbar.ActionContainers.Union(HeaderToolbar.ActionContainers.Union(ListEditorActionColumnAdapter.ActionContainers.Union(DxContextMenuAdapter.ActionContainers)));
        }
        protected override RenderFragment CreateComponent() => CustomMainFormTemplateComponent.Create(this);
        protected override void BeginUpdate() {
            base.BeginUpdate();
            ((ISupportUpdate)Toolbar)?.BeginUpdate();
        }
        protected override void EndUpdate() {
            ((ISupportUpdate)Toolbar)?.EndUpdate();
            base.EndUpdate();
        }
        public void CloseViewTemplate(ITabbedMdiDetailFormTemplate childTemplate) {
            ChildTemplates.Remove(childTemplate);
            TemplateClosed?.Invoke(this, new DetailFormTemplateChangedEventArgs(childTemplate));
        }
        public void TryAddChildTemplate(ITabbedMdiDetailFormTemplate childTemplate) {
            if (childTemplate is null) {
                return;
            }
            if (!ChildTemplates.Contains(childTemplate)) {
                ChildTemplates.Add(childTemplate);
    
                childTemplate.Activated += ChildTemplate_Activated;
                childTemplate.Closed += ChildTemplate_Closed;
            }
            SetActiveTemplateIndex(ChildTemplates.IndexOf(childTemplate));
        }
    
        private void ChildTemplate_Activated(object sender, EventArgs e) {
            var activeTemplate = (ITabbedMdiDetailFormTemplate)sender;
    
            SetActiveTemplate(activeTemplate);
            ActiveTemplateChanged?.Invoke(this, new DetailFormTemplateChangedEventArgs(activeTemplate));
        }
    
        private void OnActiveTabIndexChanged(int index) {
            if (index >= 0) {
                SetActiveTemplateIndex(index);
    
                ActiveTemplateChanged?.Invoke(this, new DetailFormTemplateChangedEventArgs(ChildTemplates[index]));
            }
        }
        private void ChildTemplate_Closed(object sender, EventArgs e) {
            var childTemplate = (ITabbedMdiDetailFormTemplate)sender;
            childTemplate.Closed -= ChildTemplate_Closed;
            childTemplate.Activated -= ChildTemplate_Activated;
    
            CloseViewTemplate(childTemplate);
        }
    
        public void RemoveChildTemplate(ITabbedMdiDetailFormTemplate childTemplate) {
            if (ChildTemplates.Contains(childTemplate)) {
                ChildTemplates.Remove(childTemplate);
    
                RefreshTabs();
            }
        }
        public void RefreshTabs() {
            ChildTemplatesChanged?.Invoke(this, EventArgs.Empty);
        }
        public void SetActiveTemplate(ITabbedMdiDetailFormTemplate childTemplate) {
            var activeIndex = ChildTemplates.IndexOf(childTemplate);
    
            if (ActiveTemplateIndex != activeIndex) {
                SetActiveTemplateIndex(activeIndex);
            }
            RefreshTabs();
        }
        public void SetActiveTemplateIndex(int index) {
            ActiveTemplateIndex = index;
            TabsModel.ActiveTabIndex = ActiveTemplateIndex;
        }
        public int ActiveTemplateIndex { get; private set; } = 0;
        public ITabbedMdiDetailFormTemplate ActiveTemplate {
            get {
                if (ActiveTemplateIndex >= 0 && ChildTemplates.Count > ActiveTemplateIndex) {
                    return ChildTemplates[ActiveTemplateIndex];
                }
                return null;
            }
        }
        public bool IsActionsToolbarVisible { get; private set; }
        public NavigateBackActionControl NavigateBackActionControl { get; }
        public AccountComponentAdapter AccountComponent { get; }
        public ShowNavigationItemActionControl ShowNavigationItemActionControl => throw new NotSupportedException();
        public CustomShowNavigationItemActionControl CustomShowNavigationItemActionControl { get; }
        public DxToolbarAdapter Toolbar { get; }
        public DxToolbarAdapter HeaderToolbar { get; }
        public ListEditorActionColumnAdapter ListEditorActionColumnAdapter { get; }
        public DxContextMenuAdapter DxContextMenuAdapter { get; }
        public string AboutInfoString { get; set; }
    
        public DxTabsModel TabsModel { get; }
        public bool IsTabbedMdi => ModelOptionsBlazor?.UIType == UIType.TabbedMDI;
        protected ObservableCollection<ITabbedMdiDetailFormTemplate> ChildTemplates { get; private set; }
        public event EventHandler ChildTemplatesChanged;
        public event EventHandler<DetailFormTemplateChangedEventArgs> ActiveTemplateChanged;
        public event EventHandler<DetailFormTemplateChangedEventArgs> TemplateClosed;
    
        void ISupportActionsToolbarVisibility.SetVisible(bool isVisible) => IsActionsToolbarVisible = isVisible;
    }
    

    Note

    The built-in ApplicationWindowTemplate and generated CustomMainFormTemplate implement the IMainFormTemplate interface that has a set of properties for UI element customization (such as ShowNavigationItemActionControl, NavigateBackActionControl, AccountComponent, HeaderToolbar, AboutInfoString). In this example, the CustomShowNavigationItemActionControl‘s type is incompatible with ShowNavigationItemActionControl. Therefore, we do not use the built-in property in this example.

  6. Modify the CustomMainFormTemplateComponent.razor component. Remove the reference to the built-in ShowNavigationItemActionControl or replace it with CustomShowNavigationItemActionControl. You can see an example in the code sample below:

    @using DevExpress.ExpressApp
    @using DevExpress.ExpressApp.Blazor
    @using DevExpress.ExpressApp.Blazor.Components
    @using DevExpress.ExpressApp.Blazor.Components.Models
    @using DevExpress.ExpressApp.Blazor.Templates
    @using DevExpress.ExpressApp.Utils
    @using Microsoft.JSInterop
    
    @inherits FrameTemplateComponentBase<CustomMainFormTemplate>
    
    <div id="main-window-template-component" class="app h-100 d-flex flex-column">
        <SkipToMainContentLink/>
        <ComponentModelObserver ComponentModel="@FrameTemplate.NavigateBackActionControl.ButtonModel">
            <div role="banner" class="header d-flex flex-row shadow-sm navbar-dark flex-nowrap @DetailViewHeaderClass @NavigateBackActionHeaderClass">
                <div class="d-flex align-items-center ps-2">
                    @FrameTemplate.CustomShowNavigationItemActionControl.GetComponentContent(@<ViewCaptionComponent WindowCaption="@ActiveTemplate" />)
                </div>
                <div class="header-right-side w-100 overflow-hidden d-flex align-items-center px-2 px-sm-3">
                    <SizeModeContainer>
                        @FrameTemplate.HeaderToolbar.GetComponentContent()
                    </SizeModeContainer>
                    <div class="d-flex ms-auto">
                        @FrameTemplate.AccountComponent.GetComponentContent()
                        <SettingsComponent />
                    </div>
                </div>
            </div>
        </ComponentModelObserver>
        <div class="xaf-flex-auto overflow-hidden d-flex">
            @if(FrameTemplate.IsTabbedMdi) {
                <div role="main" class="w-100 overflow-auto">
                    <SizeModeContainer>
                        <ComponentModelObserver ComponentModel="@FrameTemplate.TabsModel">
                            @FrameTemplate.TabsModel.GetComponentContent()
                        </ComponentModelObserver>
                    </SizeModeContainer>
                </div>
            }
            else {
                <div id="main-view-content" role="main" class="main xaf-flex-auto overflow-hidden d-flex flex-column" tabindex="-1">
                    <SizeModeContainer>
                        @if(FrameTemplate.IsActionsToolbarVisible && @FrameTemplate.Toolbar.ContainsVisibleActionControl()) {
                            <div class="main-toolbar py-3 px-2 px-sm-3">@FrameTemplate.Toolbar.GetComponentContent()</div>
                        }
                        <div class="main-content xaf-flex-auto overflow-auto pb-3 px-2 px-sm-3">
                            <ViewSiteComponent View="@FrameTemplate.View" />
                        </div>
                    </SizeModeContainer>
                </div>
            }
        </div>
    </div>
    
    @code {
        public static RenderFragment Create(CustomMainFormTemplate mainFormTemplate) => @<CustomMainFormTemplateComponent FrameTemplate="@mainFormTemplate" />;
        private string DetailViewHeaderClass => FrameTemplate.View is DetailView ? "xaf-detail-view-header" : default;
        private string NavigateBackActionHeaderClass => FrameTemplate.NavigateBackActionControl.Visible ? "xaf-show-navigate-back-action" : default;
        private string HideNavigationBtnTitle => CaptionHelper.GetLocalizedText("VisualComponents/Header", "HideNavigationPane");
        private string ShowNavigationBtnTitle => CaptionHelper.GetLocalizedText("VisualComponents/Header", "ShowNavigationPane");
        private IWindowCaption ActiveTemplate => FrameTemplate.IsTabbedMdi ? FrameTemplate.ActiveTemplate : FrameTemplate;
        private void UnsubscribeTemplateEvents() {
            if(FrameTemplate is not null) {
                FrameTemplate.ChildTemplatesChanged -= FrameTemplate_ChildTemplatesChanged;
                FrameTemplate.TemplateClosed -= FrameTemplate_TemplateClosed;
            }
        }
        private void FrameTemplate_TemplateClosed(object sender, DetailFormTemplateChangedEventArgs e) {
            InvokeAsync(StateHasChanged);
        }
        private void FrameTemplate_ChildTemplatesChanged(object sender, EventArgs e) {
            InvokeAsync(StateHasChanged);
        }
        protected override async Task InvokeAfterViewChangedJS() {
            await JSRuntime.InvokeVoidAsync("xaf.closeSideBarIfMobile");
            await base.InvokeAfterViewChangedJS();
        }
        protected override async Task OnAfterRenderAsync(bool firstRender) {
            if (firstRender) {
                await JSRuntime.InvokeVoidAsync("xaf.initApplicationWindowTemplate");
            }
            await base.OnAfterRenderAsync(firstRender);
        }
        protected override Task OnParametersSetAsync() {
            if(FrameTemplate is not null) {
                FrameTemplate.ChildTemplatesChanged += FrameTemplate_ChildTemplatesChanged;
                FrameTemplate.TemplateClosed += FrameTemplate_TemplateClosed;
            }
            return base.OnParametersSetAsync();
        }
        public override Task SetParametersAsync(ParameterView parameters) {
            UnsubscribeTemplateEvents();
    
            return base.SetParametersAsync(parameters);
        }
    }
    
  7. Override the CreateDefaultTemplate method in the YourApplicationName.Blazor.Server\BlazorApplication.cs file to use an instance of the new template:

    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.Blazor;
    using DevExpress.ExpressApp.SystemModule;
    using DevExpress.ExpressApp.Templates;
    using YourSolutionName.Blazor.Server.Templates;
    
    namespace YourSolutionName.Blazor.Server;
    
    public class YourSolutionNameBlazorApplication : BlazorApplication {
        // ...
        protected override IFrameTemplate CreateDefaultTemplate(TemplateContext context) {
            if (context == TemplateContext.ApplicationWindow) {
                return new CustomMainFormTemplate((IModelOptionsBlazor)Model.Options) { AboutInfoString = AboutInfo.Instance.GetAboutInfoString(this) };
            }
            return base.CreateDefaultTemplate(context);
        }
    }
    
  8. Run the application. XAF now renders the navigation control as a drop-down menu.

    XAF ASP.NET Core Blazor Application With a Custom Window Template, DevExpress

Note

If you want to change the template appearance or behavior of the inner tab control that contains a View (List View in this case), customize the Main Form Template.