This demo app is built using XAF Blazor and WinForms (powered by the EF Core ORM). The demo includes reusable XAF modules such as Multi-Tenancy, Security System, Reports, Scheduler, Dashboards, Office, and many custom list and property editors for real-world scenarios (charts, pivot grids, maps, data grids with master-detail and layout views).
The example application serves as the central data management hub for the fictitious company, overseeing various business entities such as Employees, Products, Orders, Quotes, Customers, and Stores. This example application is a modern multi-tenant iteration of our non-XAF WinForms Outlook-Inspired Application (dxdemo://Win/OutlookInspiredDemo - requires the DevExpress Unified Component Installer).
This multitenant application also supports the following built-in features:
- Authentication: Log in with an email/OAuth2 account (like Microsoft Entra ID or Google) and a password (the domain automatically resolves the tenant and its storage).
- Tenant Isolation and Database Creation: Multi-tenant app with multiple databases (a database per tenant). The application automatically creates a tenant database and schema at runtime (if the database does not exist).
- Host User Interface: A multi-tenant application’s operation mode for tenant list management. This mode allows a user to create, delete, and edit tenants.
- Authorization: Role-based access control (RBAC) or security rules for application administrators and end-users with restricted access rights in each tenant.
- Middle Tier Application Server: The highest protection level for XAF WinForms UI because the client application has no direct access to the database.
Before you review this XAF sample project, please take a moment to complete a short multi-tenancy related survey (share your multi-tenancy requirements with us).
When you launch the WinForms or Blazor application for the first time (, you can login using the Admin account and a blank password. The application will execute in Host User Interface mode (used to view, create and edit Tenants).
Once you log in, two tenants are created in the system: company1.com
and company2.com
. You can view the tenant list in the Host User Interface List View.
After the Host Database is initialized, you can log in to the Tenant User Interface using one of the following Tenant Administrator accounts: [email protected] and [email protected] and a blank password. A Tenant Administrator has full access to all data stored in the Tenant Database but no access to other Tenant data. Users and permissions are managed in each tenant independently.
In addition, the sample application creates a list of users with restricted access rights in each tenant (for example [email protected]).
Documentation | Getting Started | Best Practices and Limitations | Modules in a Multi-Tenant Application
In the Blazor application, the following code activates multi-tenancy.
OutlookInspired.Blazor.Server/Services/Internal/ApplicationBuilder.cs:
public static IBlazorApplicationBuilder AddMultiTenancy(this IBlazorApplicationBuilder builder, IConfiguration configuration){
builder.AddMultiTenancy()
.WithHostDbContext((_, options) => {
#if EASYTEST
string connectionString = configuration.GetConnectionString("EasyTestConnectionString");
#else
string connectionString = configuration.GetConnectionString("ConnectionString");
#endif
options.UseSqlite(connectionString);
options.UseChangeTrackingProxies();
options.UseLazyLoadingProxies();
})
.WithMultiTenancyModelDifferenceStore(e => {
#if !RELEASE
e.UseTenantSpecificModel = false;
#endif
})
.WithTenantResolver<TenantByEmailResolver>();
return builder;
}
In the WinForms application, the following code activates multi-tenancy.
OutlookInspired.Win/Services/ApplicationBuilder.cs:
public static IWinApplicationBuilder AddMultiTenancy(this IWinApplicationBuilder builder, string serviceConnectionString) {
builder.AddMultiTenancy()
.WithHostDbContext((_, options) => {
options.UseSqlite(serviceConnectionString);
options.UseChangeTrackingProxies();
options.UseLazyLoadingProxies();
})
.WithMultiTenancyModelDifferenceStore(mds => {
#if !RELEASE
mds.UseTenantSpecificModel = false;
#endif
})
.WithTenantResolver<TenantByEmailResolver>();
return builder;
}
In the Blazor application:
OutlookInspired.Blazor.Server/Services/Internal/ApplicationBuilder.cs:
// ...
builder.WithDbContext<Module.BusinessObjects.OutlookInspiredEFCoreDbContext>((serviceProvider, options) => {
// ...
options.UseSqlite(serviceProvider.GetRequiredService<IConnectionStringProvider>().GetConnectionString());
})
// ...
In the WinForms application.
OutlookInspired.Win/Services/ApplicationBuilder.cs:
// ...
builder.WithDbContext<OutlookInspiredEFCoreDbContext>((application, options) => {
// ...
options.UseSqlite(application.ServiceProvider.GetRequiredService<IConnectionStringProvider>().GetConnectionString());
}, ServiceLifetime.Transient)
// ...
A multi-tenant application works with several independent databases:
- Host database – stores a list of Super Administrators and the list of tenants.
- One or multiple tenant databases – store user data independently from other organizations (tenants).
A Tenant database is created and populated with demo data on the first login to the tenant itself.
A list of the tenants is created, and tenant databases are populated with demo data in the Module Updater:
OutlookInspired.Module/DatabaseUpdate/Updater.cs:
public override void UpdateDatabaseAfterUpdateSchema() {
base.UpdateDatabaseAfterUpdateSchema();
if (ObjectSpace.TenantName() == null) {
CreateAdminObjects();
CreateTenant("company1.com", "OutlookInspired_company1");
CreateTenant("company2.com", "OutlookInspired_company2");
ObjectSpace.CommitChanges();
}
// ...
}
private void CreateTenant(string tenantName, string databaseName) {
var tenant = ObjectSpace.FirstOrDefault<Tenant>(t => t.Name == tenantName);
if (tenant == null) {
tenant = ObjectSpace.CreateObject<Tenant>();
tenant.Name = tenantName;
tenant.ConnectionString = $"Integrated Security=SSPI;MultipleActiveResultSets=True;Data Source=(localdb)\\mssqllocaldb;Initial Catalog={databaseName}";
}
}
To determine the tenant whose database is being updated when the Module Updater executes, the Updater
class includes the TenantId
and TenantName
properties that return the current tenant's unique identifier and name respectively. When the Host Database is updated, the tenant is not specified, and these properties return null
.
The following diagram describes the application's domain architecture:
The solution consists of three distinct projects.
- OutlookInspired.Module - A platform-agnostic module required by all other projects.
- OutlookInspired.Blazor.Server - A Blazor port of the original OutlookInspired demo.
- OutlookInspired.Win - A WinForms port of the original OutlookInspired demo.
This folder serves as the centralized storage for app business logic so that all other class implementations can be compact. For instance, methods that utilize XafApplication
are located in Services/Internal/XafApplicationExtensions:
public static IObjectSpace NewObjectSpace(this XafApplication application)
=> application.CreateObjectSpace(typeof(OutlookInspiredBaseObject));
Methods that use IObjectSpace
can be found in Services/Internal/ObjectSpaceExtensions. For example:
public static TUser CurrentUser<TUser>(this IObjectSpace objectSpace) where TUser:ISecurityUser
=> objectSpace.GetObjectByKey<TUser>(objectSpace.ServiceProvider.GetRequiredService<ISecurityStrategyBase>().UserId);
The SecurityExtensions
class configures a diverse set of permissions for each department. For instance, the Management department will have:
- CRUD permissions for
EmployeeTypes
- Read-only permissions for
CustomerTypes
- Navigation permissions for
Employees
,Evaluations
, andCustomers
- Mail merge permissions for orders and customers
- Permissions for various reports including
Revenue
,Contacts
,TopSalesMan
, andLocations
.
Services/Internal/ObjectSpaceExtensions:
private static void AddManagementPermissions(this PermissionPolicyRole role)
=> EmployeeTypes.AddCRUDAccess(role)
.Concat(CustomerTypes.Prepend(typeof(ApplicationUser)).AddReadAccess(role)).To<string>()
.Concat(new[]{ EmployeeListView,EvaluationListView,CustomerListView}.AddNavigationAccess(role))
.Finally(() => {
role.AddMailMergePermission(data => new[]{ MailMergeOrder, MailMergeOrderItem, ServiceExcellence }.Contains(data.Name));
role.AddReportPermission(data => new[]{ RevenueReport, Contacts, LocationsReport, TopSalesPerson }.Contains(data.DisplayName));
})
.Enumerate();
The Attributes folder contains attribute declarations.
-
FontSizeDeltaAttribute
This attribute is applied to properties ofCustomer
,Employee
,Evaluation
,EmployeeTask
,Order
, andProduct
types to configure font size. he implementation is context-dependent; in the WinForms application, this attribute it is used by theLabelPropertyEditor
...OutlookInspired.Win/Editors/LabelControlPropertyEditor.cs:
protected override object CreateControlCore() => new LabelControl{ BorderStyle = BorderStyles.NoBorder, AutoSizeMode = LabelAutoSizeMode.None, ShowLineShadow = false, Appearance ={ FontSizeDelta = MemberInfo.FindAttribute<FontSizeDeltaAttribute>()?.Delta??0, TextOptions = { WordWrap =MemberInfo.Size==-1? WordWrap.Wrap:WordWrap.Default} } };
... and the
GridView
.OutlookInspired.Win/Services/Internal/Extensions.cs:
public static void IncreaseFontSize(this GridView gridView, ITypeInfo typeInfo){ var columns = typeInfo.AttributedMembers<FontSizeDeltaAttribute>().ToDictionary( attribute => gridView.Columns[attribute.memberInfo.BindingName].VisibleIndex, attribute => attribute.attribute.Delta); gridView.CustomDrawCell += (_, e) => { if (columns.TryGetValue(e.Column.VisibleIndex, out var column)) e.DrawCell( column); }; }
In the Blazor application,
FontSizeDeltaAttribute
dependent logic is implemented in the following extension method.OutlookInspired.Blazor.Server/Services/Internal/Extensions.cs:
public static string FontSize(this IMemberInfo info){ var fontSizeDeltaAttribute = info.FindAttribute<FontSizeDeltaAttribute>(); return fontSizeDeltaAttribute != null ? $"font-size: {(fontSizeDeltaAttribute.Delta == 8 ? "1.8" : "1.2")}rem" : null; }
The attribute is used like its WinForms application counterpart:
-
The following Conditional Appearance module attributes are in this subfolder:
DeactivateActionAttribute
: This is an extension of the Conditional Appearance module used to deactivate actions.Attributes/Appearance/DeactivateActionAttribute.cs:
puOutlookInspired.Blazor.Server/Services/Internal/Extensions.csbute { public DeactivateActionAttribute(params string[] actions) : base($"Deactivate {string.Join(" ", actions)}", DevExpress. ExpressApp.ConditionalAppearance.AppearanceItemType.Action, "1=1") { Visibility = ViewItemVisibility.Hide; TargetItems = string.Join(";", actions); } }
In much the same way, we derive from this attribute to create other attributes found in the same folder (
ForbidCRUDAttribute
,ForbidDeleteAttribute
,ForbidDeleteAttribute
). -
This folder includes attributes that extend the XAF Validation module. Available attributes include:
EmailAddressAttribute
,PhoneAttribute
,UrlAttribute
,ZipCodeAttribute
. The following code snippet illustrates how theZipCodeAttribute
is implemented. Other attributes are implemented in a similar fashion.Attributes/FontSizeDeltaAttribute.cs:
public class ZipCodeAttribute : RuleRegularExpressionAttribute { public ZipCodeAttribute() : base(@"^[0-9][0-9][0-9][0-9][0-9]$") { CustomMessageTemplate = "Not a valid ZIP code."; } }
This folder contains controllers with no dependencies:
- The
HideToolBarController
- extends the XAFIModelListView
interface with aHideToolBar
attribute so we can hide the nested list view toolbar. - The
SplitterPositionController
- extends the XAF model with aRelativePosition
property used to configure the splitter position.
This folder implements features specific to the solution.
-
This subfolder contains the CloneViewAttribute declaration, used to generate views (in addition to default views). For example:
[CloneView(CloneViewType.DetailView, LayoutViewDetailView)] [CloneView(CloneViewType.DetailView, ChildDetailView)] [CloneView(CloneViewType.DetailView, MapsDetailView)] [VisibleInReports(true)] [ForbidDelete()] public class Employee : OutlookInspiredBaseObject, IViewFilter, IObjectSpaceLink, IResource, ITravelModeMapsMarker { public const string MapsDetailView = "Employee_DetailView_Maps"; public const string ChildDetailView = "Employee_DetailView_Child"; public const string LayoutViewDetailView = "EmployeeLayoutView_DetailView"; // ... }
-
This subfolder includes Customer-related controllers, such as:
-
XAF ships with built-in mail merge support. This controller modifies the default
ShowInDocumentAction
icons. -
This controller declares an action used to display Customer Reports. (The XAF Reports module API is used).
-
-
This subfolder includes Employee-related controllers such as:
-
This subfolder includes mapping-related logic, including:
-
This controller declares map-related actions (
MapItAction
,TravelModeAction
,ExportMapAction
,PrintPreviewMapAction
,PrintAction
,StageAction
,SalesPeriodAction
) and manages associated state based onISalesMapMarker
andIRoutePointMapMarker
interfaces.
-
-
This subfolder adds platform-agnostic master-detail capabilities based on XAF's DashboardViews.
-
The
IUserControl
is implemented in a manner similar to the technique described in the following topic: How to: Include a Custom UI Control That Is Not Integrated by Default (WinForms, ASP.NET WebForms, and ASP.NET Core Blazor). The distinction lies in the addition ofUserControl
(for WinForms) and the Component (for Blazor) to aDetailView
.
-
The
-
This subfolder includes functionality specific to the Sales moduel.
-
Declares an action used to display the follow-up mail merge template for the selected order.
-
Uses a master-detail mail merge template pair to generate an invoice, converts it to a PDF, and displays it using the
PdfViewEditor
. -
These controllers declare actions used to mark the selected order as either Paid or Refunded.
-
Provides access to Order Revenue reports.
-
Adds a watermark to the Shipment Report based on order status.
-
-
This subfolder includes functionality specific to the Products module.
-
This subfolder includes functionality specific to the Quotes module.
-
This subfolder includes our implementation of a Filter manager, used by the end-user to create and save view filters.
This is a WinForms frontend project. It utilizes the previously mentioned OutlookInspired.Module
and adheres to the same folder structure.
This folder contains the following controllers with no dependencies:
-
DisableSkinsController
- This controller disables the XAF default theme-switching action. -
SplitterPositionController
- This is the WinForms implementation of the SplitterPositionController. We discussed its platform agnostic counterpart in theOutlookInspired.Module
section.
This folder contains custom controls and XAF property editors.
-
ColumnViewUserControl
- This is a base control that implements IUserControl discussed previously. -
EnumPropertyEditor
- This is a subclass of the built-inEnumPropertyEditor
(it only displays an image). -
HyperLinkPropertyEditor
- This editor displays hyperlinks with mailto support. -
LabelControlPropertyEditor
- This is an editor that renders a label. -
PdfViewerEditor
- This is a PDF viewer based on the DevExpress PDF Viewer component. -
PrintLayoutRichTextEditor
- This editor extends the built-inRichTextPropertyEditor
, but uses thePrintLayout
mode. -
ProgressPropertyEditor
- This editor is used to display progress across various contexts.
Much like the platform-agnostic module's Services Folder, our WinForms project keeps all classes as small as possible and implements business logic in extension methods.
This folder contains custom functionality specific to the solution.
-
This subfolder includes logic related to mapping.
-
MapsViewController - This controller overrides the platform-agnostic
MapsViewController
to further configure the state of map actions. -
WinMapsViewController - This is an abstract controller that provides functionality used by its derived classes -
SalesMapsViewController
andRouteMapsViewController
. The controller configures Map views for all objects that implementISalesMapsMarker
(Customer, Product) andIRouteMapsMarker
(Order, Employee) interfaces.
-
-
This subfolder contains customer module-related functionality.
- CustomerGridView, CustomerLayoutView, and CustomerStoreView: These classes derive from the previously discussed
ColumnViewUserControl
. They host customGridControl
variants, such as master-detail layouts.
- CustomerGridView, CustomerLayoutView, and CustomerStoreView: These classes derive from the previously discussed
-
This subfolder contains employee module-related functionality.
- EmployeesLayoutView - This is a descendant of
ColumnViewUserControl
that hosts a GridControl LayoutView.
- EmployeesLayoutView - This is a descendant of
-
This subfolder contains functionality related to the default XAF GridListEditor.
-
FontSizeController
- Uses theFontSizeDelta
discussed in the platfrom-agnostic module section to increase font size in row cells of an AdvancedBanded Grid. -
NewItemRowHandlingModeController
- Modifies how new object are handled when a dashboard master detail view (discussed in the platform-agnostic module section) objects are created.
-
-
This subfolder contains functionality related to products.
-
This subfolder contains opportunity module-related functionality.
This is the Blazor frontend project. It utilizes the previously mentioned OutlookInspired.Module
and maintains the same folder structure.
This folder contains Blazor components essential for project requirements.
-
ComponentBase, ComponentModelBase -
ComponentBase
is the foundation for client-side components like DxMap, DxFunnel, DXPivot, and PdfViewer. It manages loading of resources such as JavaScript files.ComponentModelBase
acts as the base model for all components, offering functionality such asClientReady
event and a hook for browser console messages, among other features. -
HyperLink, Label - TThese components mirror their WinForms counterparts and are used to render hyperlinks and labels.
-
PdfViewer - This is a
ComponentBase
descendant used to view PDF files. -
XafImg, BOImage - Both components are used to display images across a variety of contexts.
-
XafChart - This component is utilized for charting Customer store data.
-
This folder contains the
SideBySideCardView
and theStackedCardView
. They are used to display Card like list views as follows: -
This folder includes reusable .NET components, including Map, VectorMap, Funnel and Chart DevExtreme Widgets.
This folder contains the following controllers with no dependencies:
CellDisplayTemplateController
- Is an abstract controller that allows the application to render GridListEditor row cell fragments.DxGridListEditorController
- Overiddes GridListEditor behaviors (such as removing command columns).PopupWindowSizeController
- Configures the size of popup windows.
This folder contains XAF custom editors. Examples include:
-
ChartListEditor
- An abstract list editor designed to create simple object-specific variants.[ListEditor(typeof(MapItem), true)] public class MapItemChartListEditor : ChartListEditor<MapItem, string, decimal, string, XafChart<MapItem, string, decimal, string>> { public MapItemChartListEditor(IModelListView info) : base(info) { } }
-
BlazorPropertyEditor
- An abstract property editor that serves as a basis for editors such asProgressPropertyEditor
orPdfViewEditor
. The latter uses the PdfViewer component from the Components folder.Editors/BlazorPropertyEditor.cs:
[PropertyEditor(typeof(byte[]),EditorAliases.PdfViewerEditor)] public class PdfViewerEditor:BlazorPropertyEditor<PdfViewer,PdfModel>{ public PdfViewerEditor(Type objectType, IModelMemberViewItem model) : base(objectType, model){ } protected override void ReadValueCore(){ base.ReadValueCore(); ComponentModel.Bytes = (byte[])PropertyValue; } }
-
EnumPropertyEditor
- Inherits from XAF's native EnumPropertyEditor, but only displays an image (like its WinForms counterpart). -
DisplayTestPropertyEditors
- Displays raw text (like the WinForms LabelPropertyEditor).
This folder contains solution-specific functionality.
-
Uses components from
Components
(bound to data) to render customer-related data. For example, it uses theStackedCardView
with aStackedInfoCard
as shown below:Features/Customers/Stores/StoresCardView.razor:
<StackedCardView> <Content> @foreach (var store in ComponentModel.Stores){ <StackedInfoCard Body="@store.City" Image="@store.Crest.LargeImage.ToBase64Image()"/> } </Content>
The visual output is as follows:
-
Uses data-bound components from the
Components
folder to render employee-related data.Features/Employees/CardView/CardView.razor:
<StackedCardView > <Content> @foreach (var employee in ComponentModel.Objects){ <SideBySideInfoCard CurrentObject="employee" ComponentModel="@ComponentModel" Image="@employee.Picture?.Data?.ToBase64Image()" HeaderText="@employee.FullName" InfoItems="@(new Dictionary<string, string>{{ "ADDRESS", employee.Address }, { "EMAIL", $"<a href=\"mailto:{employee.Email}\">{employee.Email}</a>" },{ "PHONE", employee.HomePhone } })"/> } </Content>
Results are as follows:
The Evaluations
and Tasks
include components responsible for rendering the cell fragment in the following image. Both components are linked to the cell through Controllers\CellDisplayTemplateController
.
-
The
SchedulerGroupTypeController
is required to set up the scheduler, as follows: -
Mirroring its WinForms counterpart, this subfolder contains both the
RouteMapsViewController
and theSalesMapsViewController
. These controllers are needed to configure maps (ModalDxMap
andModalDxVectorMap
) and associated actions (such asTravelMode
,SalesPeriod
,Print
, etc). Components within this directory are fragments that use components inComponents/DevExtreme
. Additionally, they adjust height as they are displayed in a modal popup window. -
The
DetailRow
component renders the detail fragment for theOrderListView
. -
Much like the Employees subfolder, the
Component/CardViews/StackedCardView
declaration is as follow:Features/Products/CardView.razor:
<StackedCardView> <Content> @foreach (var product in ComponentModel.Objects) { <SideBySideInfoCard CurrentObject="product" ComponentModel="@ComponentModel" Image="@product.PrimaryImage.Data.ToBase64Image()" HeaderText="@product.Name" InfoItems="@(new Dictionary<string, string>{{ "COST", product.Cost.ToString("C") }, { "SALE PRICE", product.SalePrice.ToString("C") } })" FooterText="@product.Description.ToDocument(server => server.Text)"/> } </Content> </StackedCardView>
Results are as follows:
(you will be redirected to DevExpress.com to submit your response)