Table of Contents

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/​Lang​Wars

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

  1. Data fetching: The app calls the Stack Exchange API to retrieve the most-used tags, or loads a bundled offline JSON file (OfflineData.txt) via FileSystem.OpenAppPackageFileAsync().
  2. Report generation: A FlexCelReport loads the template file (report.template.xlsx), binds the data table, and runs the report to produce a filled workbook with charts.
  3. HTML export: The generated workbook is exported to HTML using FlexCelHtmlExport with embedded SVG images for crisp vector charts.
  4. Display: The HTML string is assigned to an HtmlWebViewSource and displayed in a MAUI WebView (no base64 encoding needed).
  5. Sharing: A Share button lets you send the generated .xlsx file to other apps using Share.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, ".");
        }

    }
}