LangWars (MAUI) (C# / Mobile / Maui)
Note
This demo is available in your FlexCel installation at <FlexCel Install Folder>\samples\csharp\VS2026\Mobile\Maui\LangWars and also at https://github.com/tmssoftware/TMS-FlexCel.NET-demos/tree/master/csharp/VS2026/Mobile/Maui/LangWars
Overview
This example shows how to generate a report with FlexCel using .NET MAUI. It fetches the most popular programming language tags from the Stack Overflow API, fills a pre-designed Excel template with the data, and exports the result to HTML for display in a MAUI WebView. You can switch between online and offline data in case you don't have internet access. This single project targets Android, iOS, macOS, and Windows.
How It Works
- Data fetching: The app calls the Stack Exchange API to retrieve the most-used tags, or loads a bundled offline JSON file (
OfflineData.txt) viaFileSystem.OpenAppPackageFileAsync(). - Report generation: A
FlexCelReportloads the template file (report.template.xlsx), binds the data table, and runs the report to produce a filled workbook with charts. - HTML export: The generated workbook is exported to HTML using
FlexCelHtmlExportwith embedded SVG images for crisp vector charts. - Display: The HTML string is assigned to an
HtmlWebViewSourceand displayed in a MAUIWebView(no base64 encoding needed). - Sharing: A Share button lets you send the generated
.xlsxfile to other apps usingShare.Default.RequestAsync(), which invokes the native share dialog on each platform.
FlexCel Features Demonstrated
- FlexCelReport: Template-based report generation with
AddTable()for data binding - FlexCelHtmlExport: Excel-to-HTML conversion with SVG images, HTML5 support, and embedded images
- XlsFile: Loading and saving Excel workbooks
Implementation Details
- Renderer.cs: Cross-platform logic for fetching data, running the report, and generating HTML. Uses MAUI's
FileSystem.OpenAppPackageFileAsync()to access bundled resources in a platform-independent way. - MainPage.xaml / MainPage.xaml.cs: XAML-based UI with a WebView, Fight button, Online toggle, Share button, and ActivityIndicator spinner.
Files
App.xaml.cs
using Microsoft.Extensions.DependencyInjection;
namespace LangWars
{
public partial class App : Application
{
public App()
{
InitializeComponent();
}
protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new AppShell());
}
}
}
AppShell.xaml.cs
namespace LangWars
{
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}
}
LangData.cs
using System;
using System.Drawing;
using System.Runtime.Serialization;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace LangWars;
public sealed record class LangDataList(
[property: JsonPropertyName("items")] IReadOnlyList<LangData> Items);
public sealed record class LangData(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("count")] int Count);
[JsonSerializable(typeof(LangDataList))]
internal partial class LangDataListContext : JsonSerializerContext
{
}
MainPage.xaml.cs
namespace LangWars
{
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
}
private async void OnFightClicked(object? sender, EventArgs e)
{
try
{
FightBtn.IsEnabled = false;
Spinner.IsVisible = true;
Spinner.IsRunning = true;
try
{
bool offline = !OnlineSwitch.IsToggled;
string file = await Renderer.Display(offline);
string html = await File.ReadAllTextAsync(file);
SetHTML(html);
}
finally
{
Spinner.IsRunning = false;
Spinner.IsVisible = false;
FightBtn.IsEnabled = true;
}
}
catch (Exception ex)
{
SetHTML("<html>" + System.Security.SecurityElement.Escape(ex.Message) + "</html>");
}
}
void SetHTML(string html)
{
string centered = html.Replace("<body", "<body style=\"display:flex;justify-content:center;\"");
Browser.Source = new HtmlWebViewSource { Html = centered };
}
private void OnOnlineToggled(object? sender, ToggledEventArgs e)
{
OnlineLabel.Text = e.Value ? "Online" : "Offline";
}
private async void OnShareClicked(object? sender, EventArgs e)
{
var file = Renderer.TempXlsPath;
if (!File.Exists(file))
{
bool offline = !OnlineSwitch.IsToggled;
await Renderer.Display(offline);
}
await Share.Default.RequestAsync(new ShareFileRequest
{
Title = "Share LangWars results",
File = new ShareFile(file)
});
}
}
}
MauiProgram.cs
using Microsoft.Extensions.Logging;
namespace LangWars
{
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}
}
Renderer.cs
using FlexCel.Core;
using FlexCel.Render;
using FlexCel.Report;
using FlexCel.XlsAdapter;
using System.Text.Json;
namespace LangWars
{
public static class Renderer
{
public static string TempXlsPath
{
get
{
return Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), "langwars.xlsx");
}
}
static string TempHtmlPath
{
get
{
return Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), "langwars.html");
}
}
public static async Task<string> Display(bool offline)
{
var unsorted = offline ? await LoadData() : await FetchData();
var Langs = new LangDataList(unsorted.Items.OrderByDescending(l => l.Count).ToList());
var xls = Renderer.RunReport(Langs);
Directory.CreateDirectory(Path.GetDirectoryName(TempXlsPath)!);
xls.Save(TempXlsPath); //we save it to share it and to display it on startup.
GenerateHTML(xls);
return TempHtmlPath;
}
static async Task<LangDataList> FetchData()
{
using var httpClient = new HttpClient();
var responseMessage = await httpClient.GetAsync("https://api.stackexchange.com/2.3/tags?order=desc&sort=popular&site=stackoverflow&pagesize=5");
responseMessage.EnsureSuccessStatusCode();
using var stream = await responseMessage.Content.ReadAsStreamAsync();
var result = await JsonSerializer.DeserializeAsync(stream, LangDataListContext.Default.LangDataList);
return result ?? throw new InvalidOperationException("Failed to deserialize language data from API response.");
}
static async Task<LangDataList> LoadData()
{
using var offlineData = await FileSystem.OpenAppPackageFileAsync("OfflineData.txt");
var result = await JsonSerializer.DeserializeAsync(offlineData, LangDataListContext.Default.LangDataList);
return result ?? throw new InvalidOperationException("Failed to deserialize offline language data.");
}
static ExcelFile RunReport(LangDataList langs)
{
using var rawTemplateStream = FileSystem.OpenAppPackageFileAsync("report.template.xlsx").Result; //This stream isn't seekable, but XlsFile needs a seekable stream, so we copy it to a MemoryStream.
using var templateStream = new MemoryStream();
rawTemplateStream.CopyTo(templateStream);
templateStream.Seek(0, SeekOrigin.Begin);
ExcelFile Result = new XlsFile(templateStream, true);
using (FlexCelReport fr = new(true))
{
fr.AddTable("lang", langs.Items);
fr.Run(Result);
}
return Result;
}
static void GenerateHTML(ExcelFile xls)
{
using FlexCelHtmlExport html = new(xls, true);
//If we were using png, we would have to set
//a high resolution so this looks nice in high resolution displays.
//html.ImageResolution = 326;
//but we will use SVG, which is vectorial:
html.HtmlVersion = THtmlVersion.Html_5;
html.SavedImagesFormat = THtmlImageFormat.Svg;
html.EmbedImages = true;
html.Export(TempHtmlPath, ".");
}
}
}