Table of Contents

LangWars (iOS Native) (C# / Mobile / iOS-Native)

Note

This demo is available in your FlexCel installation at <FlexCel Install Folder>\samples\csharp\VS2026\Mobile\iOS-Native\LangWars and also at https:​//​github.​com/​tmssoftware/​TMS-​FlexCel.​NET-​demos/​tree/​master/​csharp/​VS2026/​Mobile/​iOS-​Native/​Lang​Wars

Overview

This example shows how to generate a report with FlexCel on iOS. 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 WKWebView. You can switch between online and offline data in case you don't have internet access.

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) from the app bundle.
  2. Report generation: A FlexCelReport loads the template file (report.template.xlsx) from the bundle, 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 file is loaded into a WKWebView via LoadFileUrl().
  5. Sharing: A Share button lets you send the generated .xlsx file to other apps via UIDocumentInteractionController.

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: Core logic for fetching data, running the report, and generating HTML. Uses NSBundle.MainBundle.BundlePath for accessing bundled resources. Generated files are stored in InternetCache (appropriate for re-creatable content on iOS).
  • FileSender.cs: Shares the generated Excel file using UIDocumentInteractionController, presenting the share sheet from a toolbar button.
  • SceneDelegate.cs: Builds the UI programmatically with a toolbar (Fight button, Online/Offline switch, Share button) and an activity spinner during report generation. Uses InvokeOnMainThread for thread-safe UI updates.

Files

AppDelegate.cs

namespace LangWars
{
    [Register("AppDelegate")]
    public class AppDelegate : UIApplicationDelegate
    {
        public override bool FinishedLaunching(UIApplication application, NSDictionary? launchOptions)
        {
            // Override point for customization after application launch.
            return true;
        }

        public override UISceneConfiguration GetConfiguration(UIApplication application, UISceneSession connectingSceneSession, UISceneConnectionOptions options)
        {
            // Called when a new scene session is being created.
            // Use this method to select a configuration to create the new scene with.
            // "Default Configuration" is defined in the Info.plist's 'UISceneConfigurationName' key.
            return new UISceneConfiguration("Default Configuration", connectingSceneSession.Role);
        }

        public override void DidDiscardSceneSessions(UIApplication application, NSSet<UISceneSession> sceneSessions)
        {
            // Called when the user discards a scene session.
            // If any sessions were discarded while the application was not running, this will be called shortly after 'FinishedLaunching'.
            // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
        }
    }
}

FileSender.cs

namespace LangWars
{
    public static class FileSender
    {
        public static void SendFile(string fileName, UIBarButtonItem sender)
        {
            var xlsPath = Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.InternetCache),
                "langwars.xlsx");

            if (!File.Exists(xlsPath))
                return;
            UIDocumentInteractionController docController = new()
            {
                Url = NSUrl.FromFilename(fileName)
            };
            docController.PresentOptionsMenu(sender, true);
        }
    }
}

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
{
}


Main.cs

using LangWars;

// This is the main entry point of the application.
// If you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));

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(
                    Environment.GetFolderPath(Environment.SpecialFolder.InternetCache),
                    "langwars.xlsx");
            }
        }

        static string TempHtmlPath
        {
            get
            {
                return Path.Combine(
                    Environment.GetFolderPath(Environment.SpecialFolder.InternetCache),
                    "langwars.html");
            }
        }

        public static async Task<string> Display(bool offline)
        {
            var Langs = offline ? await LoadData() : await FetchData();
            Langs = new LangDataList(Langs.Items.OrderByDescending(l => l.Count).ToList());
            var xls = RunReport(Langs);
            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 = new FileStream(Path.Combine(NSBundle.MainBundle.BundlePath, "OfflineData.txt"), FileMode.Open, FileAccess.Read);
            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)
        {
            ExcelFile Result = new XlsFile(Path.Combine(NSBundle.MainBundle.BundlePath, "report.template.xlsx"), 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, ".");

            // Center the content horizontally in the webview
            var content = File.ReadAllText(TempHtmlPath);
            content = content.Replace("<body", "<body style=\"display:flex;justify-content:center\"");
            File.WriteAllText(TempHtmlPath, content);
        }

    }
}

SceneDelegate.cs

using WebKit;

namespace LangWars
{
    [Register("SceneDelegate")]
    public class SceneDelegate : UIResponder, IUIWindowSceneDelegate
    {
        WKWebView? webView;

        [Export("window")]
        public UIWindow? Window { get; set; }

        [Export("scene:willConnectToSession:options:")]
        public void WillConnect(UIScene scene, UISceneSession session, UISceneConnectionOptions connectionOptions)
        {
            // Use this method to optionally configure and attach the UIWindow 'Window' to the provided UIWindowScene 'scene'.
            // Since we are not using a storyboard, the 'Window' property needs to be initialized and attached to the scene.
            // This delegate does not imply the connecting scene or session are new (see UIApplicationDelegate 'GetConfiguration' instead).
            if (scene is UIWindowScene windowScene)
            {
                Window ??= new UIWindow(windowScene);

                var vc = new UIViewController();
                vc.View!.BackgroundColor = UIColor.SystemBackground;

                webView = new WKWebView(CGRect.Empty, new WKWebViewConfiguration());
                webView.TranslatesAutoresizingMaskIntoConstraints = false;
                vc.View.AddSubview(webView);

                NSLayoutConstraint.ActivateConstraints(new[]
                {
                    webView.TopAnchor.ConstraintEqualTo(vc.View.SafeAreaLayoutGuide.TopAnchor),
                    webView.LeadingAnchor.ConstraintEqualTo(vc.View.LeadingAnchor),
                    webView.TrailingAnchor.ConstraintEqualTo(vc.View.TrailingAnchor),
                    webView.BottomAnchor.ConstraintEqualTo(vc.View.SafeAreaLayoutGuide.BottomAnchor),
                });

                // Toolbar items
                var spinner = new UIActivityIndicatorView(UIActivityIndicatorViewStyle.Medium);
                var spinnerItem = new UIBarButtonItem(spinner);
                var onlineOfflineSwitch = new UISwitch { On = true };
                var fightButton = new UIBarButtonItem("Fight!", UIBarButtonItemStyle.Plain, (s, e) =>
                {
                    try { 
                    _ = Task.Run(async () =>
                    {
                        InvokeOnMainThread(() =>
                        {
                            vc.NavigationItem.RightBarButtonItem = spinnerItem;
                            spinner.StartAnimating();
                        });

                        bool offline = !onlineOfflineSwitch.On;
                        string filePath = await Renderer.Display(offline);

                        InvokeOnMainThread(() =>
                        {
                            spinner.StopAnimating();
                            vc.NavigationItem.RightBarButtonItem = null;
                            if (!string.IsNullOrEmpty(filePath))
                            {
                                webView.LoadFileUrl(new NSUrl(filePath, false), new NSUrl(Path.GetDirectoryName(filePath)!, false));
                            }
                        });
                    });
                    }
                    catch (Exception ex)
                    {
                        spinner.StopAnimating();
                        SetHTML("<html>" + System.Security.SecurityElement.Escape(ex.Message) + "</html>");
                    }
                });
                var shareButton = new UIBarButtonItem(UIBarButtonSystemItem.Action, (s, e) =>
                {
                    FileSender.SendFile(Renderer.TempXlsPath, (UIBarButtonItem)s!);
                });

                var switchLabel = new UILabel { Text = "Online", Font = UIFont.SystemFontOfSize(14) };
                var switchStack = new UIStackView(new UIView[] { switchLabel, onlineOfflineSwitch })
                {
                    Axis = UILayoutConstraintAxis.Horizontal,
                    Spacing = 6,
                    Alignment = UIStackViewAlignment.Center
                };
                onlineOfflineSwitch.ValueChanged += (s, e) =>
                {
                    switchLabel.Text = onlineOfflineSwitch.On ? "Online" : "Offline";
                };
                var switchItem = new UIBarButtonItem(switchStack);

                var flexSpace = new UIBarButtonItem(UIBarButtonSystemItem.FlexibleSpace);

                vc.ToolbarItems = new[] { fightButton, flexSpace, switchItem, flexSpace, shareButton };

                var nav = new UINavigationController(vc);
                nav.ToolbarHidden = false;

                Window.RootViewController = nav;
                Window.MakeKeyAndVisible();
            }
        }

        void SetHTML(string html)
        {
            webView?.LoadHtmlString(html, new NSUrl(""));            
        }


        [Export("sceneDidDisconnect:")]
        public void DidDisconnect(UIScene scene)
        {
            // Called as the scene is being released by the system.
            // This occurs shortly after the scene enters the background, or when its session is discarded.
            // Release any resources associated with this scene that can be re-created the next time the scene connects.
            // The scene may re-connect later, as its session was not neccessarily discarded (see UIApplicationDelegate `DidDiscardSceneSessions` instead).
        }

        [Export("sceneDidBecomeActive:")]
        public void DidBecomeActive(UIScene scene)
        {
            // Called when the scene has moved from an inactive state to an active state.
            // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
        }

        [Export("sceneWillResignActive:")]
        public void WillResignActive(UIScene scene)
        {
            // Called when the scene will move from an active state to an inactive state.
            // This may occur due to temporary interruptions (ex. an incoming phone call).
        }

        [Export("sceneWillEnterForeground:")]
        public void WillEnterForeground(UIScene scene)
        {
            // Called as the scene transitions from the background to the foreground.
            // Use this method to undo the changes made on entering the background.
        }

        [Export("sceneDidEnterBackground:")]
        public void DidEnterBackground(UIScene scene)
        {
            // Called as the scene transitions from the foreground to the background.
            // Use this method to save data, release shared resources, and store enough scene-specific state information
            // to restore the scene back to its current state.
        }
    }
}