This example uses a loosely coupled architecture to share codebase between desktop (WPF or WinForms) and mobile (.NET MAUI) UI clients. The application displays orders loaded from a Web service and allows users to generate a report.
Run Project
- Rebuild the solution.
- Start the WebApiService project without debugging.
- Start the WPFDesktopClient, WinFormsDesktopClient or MobileClient project.
Implementation Details
The following schema outlines application architecture:
The application includes the following projects:
- Client.Shared. Contains client-side services and common helpers.
- DataModel. A model for database objects.
- WPFDesktopClient - WPF application.
- WinFormsDesktopClient - WinForms application.
- MobileClient - .NET MAUI application.
- WebApiService - a web service that handles access to a database.
Note:
Although this example does not include authentication and role-based data access, you can use the free DevExpress Web API Service to generate a project with this capability.
Shared Client Classes
Desktop and mobile clients reuse the following classes:
OrderDataService
. Incorporates logic to communicate with the Web API service.ReportService
. Generates a report based on the selected order.
We use Dependency Injection to introduce these services into desktop and mobile projects.
WPF
C#protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
var builder = new ContainerBuilder();
builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource());
builder.RegisterType<ReportService>().As<IReportService>().SingleInstance();
builder.RegisterInstance<IOrderDataService>(new OrderDataService(new HttpClient() {
BaseAddress = new Uri("https://localhost:7033/"),
Timeout = new TimeSpan(0, 0, 10)
}));
IContainer container = builder.Build();
DISource.Resolver = (type) =>
{
return container.Resolve(type);
};
}
WinForms
C#static void Main() {
//...
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource());
builder.RegisterType<ReportService>().As<IReportService>().SingleInstance();
builder.RegisterInstance<IOrderDataService>(new OrderDataService(new HttpClient() {
BaseAddress = new Uri("https://localhost:7033/"),
Timeout = new TimeSpan(0, 0, 10)
}));
container = builder.Build();
MVVMContextCompositionRoot.ViewModelCreate += (s, e) =>
{
e.ViewModel = container.Resolve(e.RuntimeViewModelType);
};
//...
}
.NET MAUI
C#public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuilder) {
mauiAppBuilder.Services.AddTransient<OrdersViewModel>();
mauiAppBuilder.Services.AddTransient<InvoicePreviewViewModel>();
return mauiAppBuilder;
}
public static MauiAppBuilder RegisterViews(this MauiAppBuilder mauiAppBuilder) {
mauiAppBuilder.Services.AddTransient<OrdersPage>();
mauiAppBuilder.Services.AddTransient<InvoiceReportPreviewPage>();
return mauiAppBuilder;
}
public static MauiAppBuilder RegisterAppServices(this MauiAppBuilder mauiAppBuilder) {
mauiAppBuilder.Services.AddTransient<IOrderDataService>(sp => new OrderDataService(new HttpClient(MyHttpMessageHandler.GetMessageHandler()) {
//OS-specific URLs are used because Android and iOS emulators use different addresses to access the local machine: https://learn.microsoft.com/en-us/dotnet/maui/data-cloud/local-web-services?view=net-maui-7.0#local-machine-address
BaseAddress = new Uri(ON.Platform(android: "https://10.0.2.2:7033/", iOS: "https://localhost:7033/")),
Timeout = new TimeSpan(0, 0, 10)
})); ;
mauiAppBuilder.Services.AddTransient<IReportService, ReportService>();
mauiAppBuilder.Services.AddTransient<INavigationService, NavigationService>();
return mauiAppBuilder;
}
Shared Web API Service
The Web API service includes basic endpoints to retrieve orders from a database connected with Entity Framework Core:
C#public class OrdersController : ControllerBase {
//...
[HttpGet]
public async Task<ActionResult<IEnumerable<Order>>> GetOrders() {
return await _context.Orders.Include(order => order.Customer)
.Include(order => order.Items)
.ThenInclude(orderItem => orderItem.Product)
.ToListAsync();
}
}
Shared Model
Both client and server sides use the same model to work with business objects.
C#public class Customer {
//...
}
public class Order {
//...
}
public class Product {
//...
}
Files to Review
- OrderDataService.cs
- ReportService.cs
- OrdersController.cs
- DesktopClient/App.xaml.cs
- MobileClient/MauiProgram.cs
Documentation
- Get Started with DevExpress Controls for .NET Multi-platform App UI
- Get Started with DevExpress WPF Controls
- Get Started with DevExpress WinForms Controls
More Examples
Does this example address your development requirements/objectives?
(you will be redirected to DevExpress.com to submit your response)
Example Code
C#using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using DataModel;
using WebApiService;
namespace WebApiService.Controllers {
[Route("api/[controller]")]
[ApiController]
public class OrdersController : ControllerBase {
private readonly CrmContext _context;
public OrdersController(CrmContext context) {
_context = context;
}
// GET: api/Orders
[HttpGet]
public async Task<ActionResult<IEnumerable<Order>>> GetOrders() {
return await _context.Orders.Include(order => order.Customer)
.Include(order => order.Items)
.ThenInclude(orderItem => orderItem.Product)
.ToListAsync();
}
// GET: api/Orders/5
[HttpGet("{id}")]
public async Task<ActionResult<Order>> GetOrder(int id) {
var order = await _context.Orders.FindAsync(id);
if (order == null) {
return NotFound();
}
return order;
}
// PUT: api/Orders/5
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPut("{id}")]
public async Task<IActionResult> PutOrder(int id, Order order) {
if (id != order.Id) {
return BadRequest();
}
_context.Entry(order).State = EntityState.Modified;
try {
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException) {
if (!OrderExists(id)) {
return NotFound();
}
else {
throw;
}
}
return NoContent();
}
// POST: api/Orders
// To protect from overposting attacks, see https://go.microsoft.com/fwlink/?linkid=2123754
[HttpPost]
public async Task<ActionResult<Order>> PostOrder(Order order) {
_context.Orders.Add(order);
await _context.SaveChangesAsync();
return CreatedAtAction("GetOrder", new { id = order.Id }, order);
}
// DELETE: api/Orders/5
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteOrder(int id) {
var order = await _context.Orders.FindAsync(id);
if (order == null) {
return NotFound();
}
_context.Orders.Remove(order);
await _context.SaveChangesAsync();
return NoContent();
}
private bool OrderExists(int id) {
return _context.Orders.Any(e => e.Id == id);
}
}
}
C#using Autofac;
using Autofac.Features.ResolveAnything;
using Client.Shared;
using DevExpress.Utils.Design;
using DevExpress.Xpf.CodeView.Margins;
using DevExpress.Xpf.Core;
using System.Configuration;
using System.Data;
using System.Net.Http;
using System.Windows;
using System.Windows.Markup;
namespace DesktopClient {
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : System.Windows.Application {
public App() {
CompatibilitySettings.UseLightweightThemes = true;
ApplicationThemeHelper.ApplicationThemeName = LightweightTheme.Win11Light.Name;
}
IContainer container;
protected override void OnStartup(StartupEventArgs e) {
base.OnStartup(e);
ContainerBuilder builder = new ContainerBuilder();
builder.RegisterSource(new AnyConcreteTypeNotAlreadyRegisteredSource());
builder.RegisterType<ReportService>().As<IReportService>().SingleInstance();
builder.RegisterInstance<IOrderDataService>(new OrderDataService(new HttpClient() {
BaseAddress = new Uri("https://localhost:7033/"),
Timeout = new TimeSpan(0, 0, 10)
}));
container = builder.Build();
DISource.Resolver = (type) =>
{
return container.Resolve(type);
};
}
}
public class DISource : MarkupExtension {
public static Func<Type, object> Resolver { get; set; }
public Type Type { get; set; }
public override object ProvideValue(IServiceProvider serviceProvider) => Resolver?.Invoke(Type);
}
}
C#using Client.Shared;
using DataModel;
using DevExpress.Maui;
using DevExpress.Maui.Core;
using MobileClient.Services;
using MobileClient.ViewModels;
using MobileClient.Views;
using NavigationService = MobileClient.Services.NavigationService;
namespace MobileClient {
public static class MauiProgram {
public static MauiApp CreateMauiApp() {
ThemeManager.ApplyThemeToSystemBars = true;
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.RegisterViewModels()
.RegisterViews()
.RegisterAppServices()
.UseDevExpress(useLocalization: true)
.UseDevExpressCollectionView()
.UseDevExpressControls()
.UseDevExpressEditors()
.UseDevExpressPdf()
.UseDevExpressScheduler()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("univia-pro-regular.ttf", "Univia-Pro");
fonts.AddFont("roboto-bold.ttf", "Roboto-Bold");
fonts.AddFont("roboto-regular.ttf", "Roboto");
});
DevExpress.Utils.DeserializationSettings.RegisterTrustedClass(typeof(Order));
return builder.Build();
}
public static MauiAppBuilder RegisterViewModels(this MauiAppBuilder mauiAppBuilder) {
mauiAppBuilder.Services.AddTransient<OrdersViewModel>();
mauiAppBuilder.Services.AddTransient<InvoicePreviewViewModel>();
return mauiAppBuilder;
}
public static MauiAppBuilder RegisterViews(this MauiAppBuilder mauiAppBuilder) {
mauiAppBuilder.Services.AddTransient<OrdersPage>();
mauiAppBuilder.Services.AddTransient<InvoiceReportPreviewPage>();
return mauiAppBuilder;
}
public static MauiAppBuilder RegisterAppServices(this MauiAppBuilder mauiAppBuilder) {
mauiAppBuilder.Services.AddTransient<IOrderDataService>(sp => new OrderDataService(new HttpClient(MyHttpMessageHandler.GetMessageHandler()) {
BaseAddress = new Uri(ON.Platform(android: "https://10.0.2.2:7033/", iOS: "https://localhost:7033/")),
Timeout = new TimeSpan(0, 0, 10)
})); ;
mauiAppBuilder.Services.AddTransient<IReportService, ReportService>();
mauiAppBuilder.Services.AddTransient<INavigationService, NavigationService>();
return mauiAppBuilder;
}
}
}