This example demonstrates how you can create a Web API service backend and a mobile .NET MAUI application frontend. The backend uses EF Core for data access.
The application works with blog post data. It authenticates a user, determines his or her permissions, and selectively enables the following data operations:
- Lists existing Post records
- Displays a photo of a Post author
- Creates new Post records
- Archives a Post record
- Displays a report based on Post records
- Visual Studio 2022 v17.0+
- .NET SDK 6.0+
- DevExpress .NET MAUI Project Templates for Visual Studio 2022
- DevExpress Libraries v22.2+. Download and run our Unified Component Installer. Make sure to enable the Cross-Platform .NET App UI & Web API service (XAF) option in the list of products to install. The installer will register local NuGet package sources and Visual Studio templates required for this tutorial.
You don't have to use the DevExpress Unified Component Installer if you only want to run the example project or use the project as a boilerplate for your application. You can manually register your NuGet feed URL in Visual Studio as described in the following article: Setup Visual Studio's NuGet Package Manager.
NOTE
If you’ve used a pre-release version of our components or obtained a hotfix from DevExpress, NuGet packages will not be restored automatically (you will need to update them manually). For more information, please refer to the following article: Updating Packages. Remember to enable the Include prerelease option.
- In Visual Studio, create a new project. Use the DevExpress XAF Template Gallery template. Enter appropriate project-related information and start the Wizard. On the first page, select only the following tile: Service (ASP.NET Core Web API).
- Select Entity Framework as your ORM.
- Select Standard Authentication to generate relevant JWT authentication scaffolding code.
- If you own a DevExpress Universal Subscription, the next page allows you to select additional modules for your Web API Service. Make certain to select Reports as this module is required to complete the tutorial (last step). You can select other modules if you plan to extend the application. Click Finish.
-
Modify the
WebAPI/Properties/launchSettings.json
file and remove the IIS Express profile so thatKestrel server ports
will be utilized. Once complete, the file's"profiles"
section should appear as shown below:Properties/launchSettings.json:
"profiles": { "WebAPI.WebApi": { "commandName": "Project", "dotnetRunMessages": "true", "launchBrowser": true, "launchUrl": "swagger", "applicationUrl": "https://localhost:5001;http://localhost:5000", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } }
For more information, refer to the following DevExpress help topic: Create a Standalone Web API Application.
-
Declare the
Post
object.BusinessObjects/Post.cs
[VisibleInReports] public class Post : BaseObject { public virtual string Title { get; set; } public virtual string Content { get; set; } public virtual ApplicationUser Author { get; set; } public override void OnCreated() { base.OnCreated(); Author = ObjectSpace.FindObject<ApplicationUser>(CriteriaOperator.Parse("ID=CurrentUserId()")); } }
In the code sample above,
Post
class inherits BaseObject to simplify data model implementation. This tutorial makes use of the followingBaseObject
features:- Predefined
Guid
-type primary key field (ID
) OnCreated
lifecycle methodObjectSpace
property (allows you to communicate with the underlying data layer)
For more information, refer to the following DevExpress help topic: BaseObjectSpace.
- Predefined
-
Modify the Entity Framework DBContext with an additional DbSet.
BusinessObjects\WebAPIDbContext.cs:
public DbSet<Post> Posts { get; set; }
-
Modify the
Startup.cs
file to registerbuilt-in CRUD endpoints
for the Post object.Startup.cs:
services .AddXafWebApi(Configuration, options => { // Make your business objects available in the Web API and generate the GET, POST, PUT, and DELETE HTTP methods for it. // options.BusinessObject<YourBusinessObject>(); options.BusinessObject<Post>(); });
The XAF Solution Wizard generates the connection string and startup code required to store persistent data in a SQL Server Express LocalDB database (only available for Microsoft Windows). If you are planning to develop your Web API backend on a non-Windows machine, consider using SQLite instead.
To use SQLite, add the Microsoft.EntityFrameworkCore.Sqlite v6 NuGet package to your project's dependencies. Once complete, add the following code to the ConfigureServices
method within Startup.cs
:
Startup.cs:
public void ConfigureServices(IServiceCollection services) {
// ...
services.AddDbContextFactory<WebAPIEFCoreDbContext>((serviceProvider, options) => {
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
string connectionString = Configuration.GetConnectionString("ConnectionString");
options.UseSqlServer(connectionString);
}
else {
string sqliteDBPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "WebAPIDemo.db");
options.UseSqlite($"Data Source={sqliteDBPath}");
}
// ...
}
}
-
Open the
WebAPI/DatabaseUpdate/Updater.cs
file and add the following code to theUpdateDatabaseAfterUpdateSchema
method to create appropriate users (Editor and Viewer), assign roles/permissions, and create sample Post objects:DatabaseUpdate\Updater.cs:
var editorUser = ObjectSpace.FirstOrDefault<ApplicationUser>(user=>user. UserName=="Editor")??ObjectSpace.CreateObject<ApplicationUser>(); if (ObjectSpace.IsNewObject(editorUser)) { //create Editor User/Role editorUser.UserName="Editor"; var editorRole = ObjectSpace.CreateObject<PermissionPolicyRole>(); editorRole.Name = "EditorRole"; editorUser.SetPassword(""); editorRole.AddTypePermission<Post>(SecurityOperations.CRUDAccess, SecurityPermissionState.Allow); editorRole.AddTypePermission<ApplicationUser>(SecurityOperations.CRUDAccess, SecurityPermissionState.Allow); editorUser.Roles.Add(editorRole); editorUser.Roles.Add(defaultRole); //create Viewer User/Role var viewerUser = ObjectSpace.CreateObject<ApplicationUser>(); viewerUser.UserName = "Viewer"; var viewerRole = ObjectSpace.CreateObject<PermissionPolicyRole>(); viewerRole.Name = "ViewerRole"; viewerRole.AddTypePermission<Post>(SecurityOperations.Read, SecurityPermissionState.Allow); viewerRole.AddTypePermission<ApplicationUser>(SecurityOperations.Read, SecurityPermissionState.Allow); viewerUser.Roles.Add(viewerRole); viewerUser.Roles.Add(defaultRole); //commit ObjectSpace.CommitChanges(); //assign authentication type foreach (var user in new[] { editorUser, viewerUser }. Cast<ISecurityUserWithLoginInfo>()) { user.CreateUserLoginInfo(SecurityDefaults.PasswordAuthentication, ObjectSpace.GetKeyValueAsString(user)); } //sample posts var post = ObjectSpace.CreateObject<Post>(); post.Title = "Hello World"; post.Content = "This is a FREE API for everybody"; post.Author=editorUser; post = ObjectSpace.CreateObject<Post>(); post.Title = "Hello MAUI"; post.Content = "Please smash the like button to help our videos get discovered"; post.Author=editorUser; }
At this point, you can run your Web API service and use the Swagger interface to authenticate as previously defined users and test generated endpoints (for example, query available posts). Refer to the following article for additional information Test the Web API with Swagger or Postman.
NOTE
Debugging configurations for both iOS and Android can be complex, so it is not feasible to document all possible debugging scenarios. We tested our sample app on Windows 10 with Visual Studio 2022. For iOS tests, we paired a remote Mac. For Android tests, we utilized the built-in emulator.
- Open the Create a new project window in Visual Studio. Select the DevExpress .NET MAUI option and start the wizard.
- Choose both the iOS & Android platforms, the Tabbed layout, and the following controls: Collection View, Data Editors, and Data Forms.
-
In the
Platform/Android/AndroidManifest.xml
file, add the following code to set up the file provider (needed by the application to store downloaded reports):Platform/Android/AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" /> <application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"> <provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> </provider> </application> <!-- ... --> </manifest>
-
Add an
AndroidMessageHandler
class definition toPlatform/Android/AndroidMessageHandler.cs
. You will use this class to create an HTTP Client object that exchanges messages with the backend._Platform/Android/AndroidMessageHandler.cs:
using MAUI.Platforms.Android; namespace MAUI.Platforms.Android { class AndroidMessageHandler : Xamarin.Android.Net.AndroidMessageHandler { public AndroidMessageHandler() => ServerCertificateCustomValidationCallback = (_, cert, _, errors) => cert is { Issuer: "CN=localhost" } || errors == System.Net. Security.SslPolicyErrors.None; protected override Javax.Net.Ssl.IHostnameVerifier GetSSLHostnameVerifier (Javax.Net.Ssl.HttpsURLConnection connection) => new HostnameVerifier(); private sealed class HostnameVerifier : Java.Lang.Object, Javax.Net.Ssl. IHostnameVerifier { public bool Verify(string hostname, Javax.Net.Ssl.ISSLSession session) => Javax.Net.Ssl.HttpsURLConnection.DefaultHostnameVerifier!. Verify(hostname, session) || hostname == "10.0.2.2" && session.PeerPrincipal?.Name == "CN=localhost"; } } } namespace MAUI.Services { public static partial class HttpMessageHandler { static HttpMessageHandler() => PlatformHttpMessageHandler = new AndroidMessageHandler(); } }
-
The same functionality is needed for iOS applications. Create an
IOSMessageHandler
class in thePlatform/IOS
folder.Platform/IOS/IOSMessageHandler.cs:
public static partial class HttpMessageHandler { static HttpMessageHandler() { NSUrlSessionHandler nSUrlSessionHandler = new(); nSUrlSessionHandler.ServerCertificateCustomValidationCallback += (_, cert, _, errors) => cert is { Issuer: "CN=localhost" } || errors == System.Net. Security.SslPolicyErrors.None; nSUrlSessionHandler.TrustOverrideForUrl = (sender, url, trust) => { return true; }; PlatformHttpMessageHandler = nSUrlSessionHandler; } }
-
Add a definition for the
Post
class in theModel/Post.cs
file.Model/Post.cs:
public class Post { [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public Guid ID { get; set; } public string Title { get; set; } public string Content { get; set; } }
-
Refactor the
IDataStore
interface inServices/IDataStore.cs
so it declares aGetItemsAsync
method used to obtain the list of existing posts.Services/IDataStore.cs:
public interface IDataStore<T> { Task<IEnumerable<T>> GetItemsAsync(bool forceRefresh = false); }
-
Configure the
BaseViewModel
to use the data store:ViewModels/BaseViewModel.cs:
public class BaseViewModel : INotifyPropertyChanged { public IDataStore<Post> DataStore => DependencyService.Get<IDataStore<Post>>(); // ... }
-
Add a
WebAPIService
class to the project (theServices/WebAPIService.cs
file). In this class, implement theIDataStore
interface and theAuthenticate
method. In code, send HTTP requests to your Web API service to authenticate and obtain data as follows:Services/WebAPIService.cs:
public static partial class HttpMessageHandler { static readonly System.Net.Http.HttpMessageHandler PlatformHttpMessageHandler; public static System.Net.Http.HttpMessageHandler GetMessageHandler() => PlatformHttpMessageHandler; } public class WebAPIService : IDataStore<Post> { private static readonly HttpClient HttpClient = new(HttpMessageHandler.GetMessageHandler()) { Timeout = new TimeSpan(0, 0, 10) }; private readonly string _apiUrl = ON.Platform(android: "https://10.0.2.2:5001/api/", iOS: "https://localhost:5001/api/"); private readonly string _postEndPointUrl; private const string ApplicationJson = "application/json"; public WebAPIService() => _postEndPointUrl = _apiUrl + "odata/" + nameof(Post); public async Task<IEnumerable<Post>> GetItemsAsync(bool forceRefresh = false) => await RequestItemsAsync(); private async Task<IEnumerable<Post>> RequestItemsAsync(string query = null) => JsonNode.Parse(await HttpClient.GetStringAsync($"{_postEndPointUrl}{query}"))!["value"].Deserialize<IEnumerable<Post>>(); public async Task<string> Authenticate(string userName, string password) { var tokenResponse = await RequestTokenAsync(userName, password); var reposeContent = await tokenResponse.Content.ReadAsStringAsync(); if (tokenResponse.IsSuccessStatusCode) { HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", reposeContent); return string.Empty; } return reposeContent; } private async Task<HttpResponseMessage> RequestTokenAsync(string userName, string password) { try { return await HttpClient.PostAsync($"{_apiUrl}Authentication/Authenticate", new StringContent(JsonSerializer.Serialize(new { userName, password = $"{password}" }), Encoding.UTF8, ApplicationJson)); } catch (Exception) { return new HttpResponseMessage(System.Net.HttpStatusCode.BadGateway) { Content = new StringContent("An error occurred during the processing of the request. Please consult the demo's ReadMe file (Step 1,10) to discover potential causes and find solutions.") }; } } }
NOTE
If you are developing on a Windows PC and using a remote Mac to build and run the simulator, localhost will not resolve to the Web API service host machine. Multiple solution guides are available to address this issue, including: Accessing ASP.NET Core API hosted on Kestrel over Https from iOS Simulator, Android Emulator and UWP Applications.
You may experience a similar problem with your Android project. Review to the following GitHub issue for more information: Cannot access the Web API server when debugging the application using a real device (Android) connected through USB.
-
Register
WebAPIService
, routes, and initial navigation inApp.xaml.cs
.App.xaml.cs:
public App() { InitializeComponent(); DependencyService.Register<NavigationService>(); DependencyService.Register<WebAPIService>(); Routing.RegisterRoute(typeof(ItemsPage).FullName, typeof(ItemsPage)); MainPage = new MainPage(); var navigationService = DependencyService.Get<INavigationService>(); navigationService.NavigateToAsync<LoginViewModel>(true); }
-
Modify your
LoginViewModel
(to authenticate in the Web API service) and navigate to the Items page after successful authentication.ViewModels/LoginViewModel.cs:
async void OnLoginClicked() { IsAuthInProcess = true; var response = await ((WebAPIService)DataStore).Authenticate(userName, password); IsAuthInProcess = false; if (!string.IsNullOrEmpty(response)) { ErrorText = response; HasError = true; return; } HasError = false; await Navigation.NavigateToAsync<ItemsViewModel>(); }
-
In the
ItemsViewModel
, modify theExecuteLoadItemsCommand
to request all posts from theIDataStore
service.ViewModels/ItemsViewModel.cs:
async Task ExecuteLoadItemsCommand() { IsBusy = true; try { Items.Clear(); var items = await DataStore.GetItemsAsync(true); foreach (var item in items) { Items.Add(item); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine(ex); } finally { IsBusy = false; } }
Start the Web API project, and then your .NET MAUI app. Log in as an Editor or Viewer (users created in the Web API Module Updater).
Once logged in, you will see the predefined posts displayed in a list view.
-
Implement a Web API service endpoint to serve the author's photo based on post ID information.
First, add the
Photo
property to theApplicationUser
persistent class:BusinessObjects/ApplicationUser.cs:
public class ApplicationUser : PermissionPolicyUser, ISecurityUserWithLoginInfo { // ... public virtual MediaDataObject Photo { get; set; } }
Modify Module Updater. Add necessary logic to assign photos to predefined users:
DatabaseUpdate\Updater.cs:
// ... editorUser.Photo = ObjectSpace.CreateObject<MediaDataObject>(); editorUser.Photo.MediaData = GetResourceByName("Janete"); viewerUser.Photo = ObjectSpace.CreateObject<MediaDataObject>(); viewerUser.Photo.MediaData = GetResourceByName("John"); // ...
NOTE
In our example code, the
GetResourceByName
method returns a byte array representation of an account image based on name. You can find an implementation for this method in the Web API project's DatabaseUpdate/Updater.cs file. Note that this implementation requires the image resources to be compiled into the application's assembly (the .jpg files'Build Action
option must be set toEmbedded resource
).Add a
CustomEndPointController
inside theWebAPI/API
directory, and inject theISecurityProvider
andIObjectSpaceFactory
. Implement a controller action that serves post author photos as follows.API/CustomEndPointController.cs:
[ApiController] [Route("api/[controller]")] [Authorize] public class CustomEndPointController : ControllerBase { private readonly ISecurityProvider _securityProvider; private readonly IObjectSpaceFactory _securedObjectSpaceFactory; public CustomEndPointController(ISecurityProvider securityProvider, IObjectSpaceFactory securedObjectSpaceFactory) { _securityProvider = securityProvider; _securedObjectSpaceFactory = securedObjectSpaceFactory; } [HttpGet("AuthorPhoto/{postId}")] public FileStreamResult AuthorPhoto(Guid postId) { using var objectSpace = _securedObjectSpaceFactory.CreateObjectSpace(typeof(Post)); var post = objectSpace.GetObjectByKey<Post>(postId); var photoBytes = post.Author.Photo.MediaData; return File(new MemoryStream(photoBytes), "application/octet-stream"); } }
-
In the .NET MAUI project, update the data service with methods that request post details as well as the author's photo.
Services/IDataStore.cs:
public interface IDataStore<T> { // ... Task<T> GetItemAsync(string id); Task<byte[]> GetAuthorPhotoAsync(Guid postId); }
Services/WebAPIService.cs:
public class WebAPIService : IDataStore<Post> { // ... public async Task<Post> GetItemAsync(string id) => (await RequestItemsAsync($"?$filter={nameof(Post.ID)} eq {id}")).FirstOrDefault(); public async Task<byte[]> GetAuthorPhotoAsync(Guid postId) => await HttpClient.GetByteArrayAsync($"{_apiUrl}CustomEndPoint/AuthorPhoto/{postId}"); }
-
Implement logic required to display the detail view when a user taps a post in the list.
ViewModels/ItemDetailViewModel.cs:
public ImageSource Thumbnail => ImageSource.FromStream(() => new MemoryStream(_photoBytes)); public async Task LoadItemId(string itemId) { try { _photoBytes = await DataStore.GetAuthorPhotoAsync(Guid.Parse(itemId)); OnPropertyChanged(nameof(Thumbnail)); Post = await DataStore.GetItemAsync(itemId); Id = Post.ID; Title = Post.Title; Content = Post.Content; } catch (Exception e) { System.Diagnostics.Debug.WriteLine($"Failed to Load Post {e}"); } }
ViewModels/ItemsViewModel.cs:
public class ItemsViewModel : BaseViewModel { Post _selectedPost; public ItemsViewModel() { // ... ItemTapped = new Command<Post>(OnItemSelected); } public Command<Post> ItemTapped { get; } public Post SelectedPost { get => _selectedPost; set { SetProperty(ref _selectedPost, value); OnItemSelected(value); } } async void OnItemSelected(Post post) { if (post != null) await Navigation.NavigateToAsync<ItemDetailViewModel>(post.ID); } // ... }
App.xaml.cs:
public App() { // ... Routing.RegisterRoute(typeof(ItemDetailPage).FullName, typeof(ItemDetailPage)); // ... }
Views/ItemDetailPage.xaml:
<StackLayout Spacing="5" Padding="15"> <Label Text="Title:" FontFamily="Roboto" FontSize="12" TextColor="{StaticResource NormalLightText}"/> <Label Text="{Binding Title}" FontFamily="Roboto" FontSize="14" TextColor="{StaticResource NormalText}" Margin="0, 0, 0, 15"/> <Label Text="Content:" FontFamily="Roboto" FontSize="12" TextColor="{StaticResource NormalLightText}" /> <Label Text="{Binding Content}" FontFamily="Roboto" FontSize="14" TextColor="{StaticResource NormalText}"/> <Label Text="Author:"></Label> <Image Source="{Binding Thumbnail}"></Image> </StackLayout>
-
The Web API Service automatically generates OData endpoints required to create business objects. However, the example application's security system is configured to disallow post creation for certain users. To check permissions before a user can try and submit a post, implement a custom
CanCreate
endpoint.API/CustomEndPointController.cs:
[HttpGet(nameof(CanCreate))] public IActionResult CanCreate(string typeName) { var strategy = (SecurityStrategy)_securityProvider.GetSecurity(); var objectType = strategy.TypesInfo.PersistentTypes.First(info => info.Name == typeName).Type; return Ok(strategy.CanCreate(objectType)); }
-
Add new methods to the .NET MAUI application's data service: one will check user permissions, another will submit a new post.
Services/IDataStore.cs:
public interface IDataStore<T> { // ... Task<bool> UserCanCreatePostAsync(); Task<T> AddItemAsync(Post post); }
Services/WebAPIService.cs:
public class WebAPIService : IDataStore<Post> { // ... public async Task<bool> UserCanCreatePostAsync() => (bool)JsonNode.Parse(await HttpClient.GetStringAsync($"{_apiUrl} CustomEndpoint/CanCreate?typename=Post")); public async Task<bool> AddItemAsync(Post post) { var httpResponseMessage = await HttpClient.PostAsync(_postEndPointUrl, new StringContent(JsonSerializer.Serialize(post), Encoding.UTF8, ApplicationJson)); if (!httpResponseMessage.IsSuccessStatusCode) { await Shell.Current.DisplayAlert("Error", await httpResponseMessage.Content.ReadAsStringAsync(), "OK"); } return httpResponseMessage.IsSuccessStatusCode; } }
-
Configure views and navigation:
ViewModels/ItemsViewModel.cs:
public class ItemsViewModel : BaseViewModel { public ItemsViewModel() { // ... AddItemCommand = new Command(OnAddItem); } public Command AddItemCommand { get; } async void OnAddItem(object obj) { if (await DataStore.UserCanCreatePostAsync()) { await Navigation.NavigateToAsync<NewItemViewModel>(null); } else { await Shell.Current.DisplayAlert("Error", "Access denied", "Ok"); } } }
App.xaml.cs:
public App() { // ... Routing.RegisterRoute(typeof(NewItemPage).FullName, typeof(NewItemPage)); // ... }
Views/ItemsPage.xaml:
<ContentPage.ToolbarItems > <ToolbarItem Text="Add" Command="{Binding AddItemCommand}" /> </ContentPage.ToolbarItems>
-
In the Web API service, create an
Archive
endpoint. This endpoint will obtain a post from the database and archive the post to disk. The following controller action implementation usessecuredObjectSpaceFactory
to communicate with the data store so that all data operations respect appropriate security permissions.API/CustomEndPointController.cs:
[HttpPost(nameof(Archive))] public async Task<IActionResult> Archive([FromBody] Post post) { using var objectSpace = _securedObjectSpaceFactory.CreateObjectSpace<Post>(); post = objectSpace.GetObject(post); var photo = post.Author.Photo.MediaResource.MediaData; await System.IO.File.WriteAllTextAsync($"{post.ID}", JsonSerializer.Serialize(new { photo, post.Title, post.Content, post.Author.UserName })); return Ok(); }
-
Extend the .NET MAUI application's data service with a method that archives a post.
Services/IDataStore.cs:
public interface IDataStore<T> { // ... Task ArchivePostAsync(T post); }
Services/WebAPIService.cs:
public class WebAPIService : IDataStore<Post> { // ... public async Task ArchivePostAsync(Post post) { var httpResponseMessage = await HttpClient.PostAsync($"{_apiUrl}CustomEndPoint/Archive", new StringContent(JsonSerializer.Serialize(post), Encoding.UTF8, ApplicationJson)); if (httpResponseMessage.IsSuccessStatusCode) { await Shell.Current.DisplayAlert("Success", "This post is saved to disk", "Ok"); } else { await Shell.Current.DisplayAlert("Error", await httpResponseMessage.Content.ReadAsStringAsync(), "Ok"); } } }
-
Configure the detail view: add a UI element that initiates the Archive command.
ViewModels/ItemDetailViewModel.cs:
public class ItemDetailViewModel : BaseViewModel, IQueryAttributable { public ItemDetailViewModel() => ArchiveCommand = new Command(OnArchive); private async void OnArchive(object obj) { if (!await DataStore.UserCanCreatePostAsync()) { await Shell.Current.DisplayAlert("Error", "Permission denied", "OK"); } else { await DataStore.ArchivePostAsync(Post); } } public Command ArchiveCommand { get; } }
Views/ItemDetailPage.xaml
<ContentPage.ToolbarItems > <ToolbarItem Text="Archive" Command="{Binding ArchiveCommand}"></ToolbarItem> </ContentPage.ToolbarItems>
The XAF Reports module ships as part of the DevExpress Universal Subscription. You can use it to easily integrate DevExpress Reports into your backend Web API service. You can skip this step if you do not own the DevExpress Universal Subscription.
To create and initialize a report:
-
Add a DevExpress Report component using the Visual Studio New Item wizard.
-
Drag and drop a
CollectionDataSource
component from the Visual Studio toolbox and change itsObjectTypeName
toWebAPI.BusinessObjects.Post
. -
Drag & drop all discovered fields from the Field List window onto the Report details surface.
-
Use a predefined reports updater to initialize the report.
Module.cs:
public override IEnumerable<ModuleUpdater> GetModuleUpdaters(IObjectSpace objectSpace, Version versionFromDB) {
var predefinedReportsUpdater = new PredefinedReportsUpdater(Application, objectSpace, versionFromDB);
predefinedReportsUpdater.AddPredefinedReport<XtraReport1>("Post Report",typeof(Post));
return new ModuleUpdater[] { new DatabaseUpdate.Updater(objectSpace, versionFromDB),predefinedReportsUpdater };
}
For more information on report generation, refer to the following DevExpress help topic: Create a Report in Visual Studio.
For more information on the use of predefined static reports in XAF, refer to the following DevExpress help topic: Create Predefined Static Reports.
Watch video: Preview Reports as PDF in .NET MAUI Apps using Backend Web API service Endpoints with EF Core
-
Create a
GetReport
endpoint. It redirects to the built-inDownloadByName
endpoint and returns a Report with title Post Report.API/CustomEndPointController.cs:
[HttpGet(nameof(GetReport))] public RedirectResult GetReport() => Redirect("~/api/report/DownloadByName(Post Report)");
-
In the MAUI application's data service, implement a method that downloads the report:
public interface IDataStore<T> { // ... Task ShapeIt();; }
Services/WebAPIService.cs:
public async Task ShapeIt() { var bytes = await HttpClient.GetByteArrayAsync($"{_apiUrl}report/DownloadByName (Post Report)"); #if ANDROID var fileName = $"{FileSystem.Current.AppDataDirectory}/Report.pdf"; await File.WriteAllBytesAsync(fileName, bytes); var intent = new Android.Content.Intent(Android.Content.Intent.ActionView); intent.SetDataAndType(AndroidX.Core.Content.FileProvider.GetUriForFile(Android.App. Application.Context, $"{Android.App.Application.Context.ApplicationContext?.PackageName}.provider", new Java.IO.File(fileName)),"application/pdf"); intent.SetFlags(Android.Content.ActivityFlags.ClearWhenTaskReset | Android.Content.ActivityFlags.NewTask | Android.Content.ActivityFlags.GrantReadUriPermission); Android.App.Application.Context.ApplicationContext?.StartActivity(intent); #else var path = Environment.GetFolderPath (Environment.SpecialFolder.Personal); var fileName = $"{path}/Report.pdf"; await File.WriteAllBytesAsync(fileName, bytes); var filePath = Path.Combine(path, "Report.pdf"); var viewer = UIKit.UIDocumentInteractionController.FromUrl(Foundation.NSUrl.FromFilename(filePath)); viewer.PresentOpenInMenu(new System.Drawing.RectangleF(0,-260,320,320),Platform. GetCurrentUIViewController()!.View! , true); #endif }
-
Update the List view: add a command that displays the report.
Views/ItemsPage.xaml
<ContentPage.ToolbarItems > <ToolbarItem Text="ShapeIt" Command="{Binding ShapeItCommand}" /> </ContentPage.ToolbarItems>
ViewModels/ItemsViewModel.cs:
public class ItemsViewModel : BaseViewModel { public ItemsViewModel() { // ... ShapeItCommand = new Command(async () => await DataStore.ShapeIt()); } public Command ShapeItCommand { get; } }