Example T1230298
Visible to All Users

WPF or WinForms & .NET MAUI - Share Code Between Desktop and Mobile Projects

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.

Demo Video

Run Project

  1. Rebuild the solution.
  2. Start the WebApiService project without debugging.
  3. Start the WPFDesktopClient, WinFormsDesktopClient or MobileClient project.

Implementation Details

The following schema outlines application architecture:

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

Documentation

More Examples

Does this example address your development requirements/objectives?

(you will be redirected to DevExpress.com to submit your response)

Example Code

Client.Shared/OrderDataService.cs
C#
using DataModel; using System.Collections.ObjectModel; using System.ComponentModel.DataAnnotations.Schema; using System.Net.Http.Headers; using System.Text.Json.Nodes; using System.Text; using System.Net.Http; using System.Text.Json; namespace Client.Shared { public class OrderDataService : IOrderDataService { readonly HttpClient client; public OrderDataService(HttpClient httpClient) { client = httpClient; } public async Task<List<Order>> GetOrdersAsync() { List<Order> orders = null; try { HttpResponseMessage response = await client.GetAsync("api/Orders"); if (response.IsSuccessStatusCode) { orders = await response.Content.ReadAsAsync<List<Order>>(); } } catch (Exception) { } return orders; } } public interface IOrderDataService { Task<List<Order>> GetOrdersAsync(); } }
Client.Shared/ReportService.cs
C#
using DataModel; using DevExpress.DataAccess.ObjectBinding; using DevExpress.XtraRichEdit.Model; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Client.Shared { public class ReportService : IReportService { public async Task<string> ExportInvoiceReportToPdfAsync(Order order, string baseFolder) { InvoiceReport report = await GenerateInvoiceReportAsync(order); string resultFile = Path.Combine(baseFolder, report.Name + ".pdf"); await report.ExportToPdfAsync(resultFile); return resultFile; } public async Task<InvoiceReport> GenerateInvoiceReportAsync(Order order) { return await Task.Run(async () => { InvoiceReport invoiceReport = new InvoiceReport() { Name = $"Invoice_{order.Id}" }; ObjectDataSource objectDataSource = new ObjectDataSource(); objectDataSource.DataSource = order; invoiceReport.DataSource = objectDataSource; await invoiceReport.CreateDocumentAsync(); return invoiceReport; }); } } public interface IReportService { Task<InvoiceReport> GenerateInvoiceReportAsync(Order order); Task<string> ExportInvoiceReportToPdfAsync(Order order, string baseFolder); } }
WebApiService/Controllers/OrdersController.cs
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); } } }
DesktopClient/App.xaml.cs
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); } }
MobileClient/MauiProgram.cs
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; } } }

Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.