TBC Macros and Extensions

 View Only
Expand all | Collapse all

TML Development Environment for TBC 2025.21

  • 1.  TML Development Environment for TBC 2025.21

    Posted 25 days ago
    Hello TML Community,

    I'm looking for links to any updated walkthroughs to establish a development environment For TML using TBC 2025.21:
    • Do I still have to use Visual Studio 2019, or can I use a newer build to get IronPython support?
    • Looking for updates for example project to review and learn from, example implementations?

    Apologies if this has been answered multiple times.  I watch the thread weekly looking for any updates for a new to TML walkthrough, but I have not found anything or simply missed it.

    Chris Siebern, PE, PS

    Application Success Manager Survey / Geospatial | Design Technology Services

    Associate

    chris.siebern@collierseng.com

    Main: 877 627 3772

    Remote

    LinkedIn Twitter YouTube Instagram Facebook

    Building for Our Future - See our 2023 Impact Report

    DISCLAIMER This e-mail is confidential. It may also be legally privileged. If you are not the addressee you may not copy, forward, disclose or use any part of this email text or attachments. If you have received this message in error, please delete it and all copies from your system and notify the sender immediately by return e-mail. Internet communications cannot be guaranteed to be timely, secure, error or virus free. The sender does not accept liability for any errors or omissions. Any drawings, sketches, images, or data are to be understood as copyright protected.



  • 2.  RE: TML Development Environment for TBC 2025.21

    Posted 24 days ago
    Edited by Ronny Schneider 24 days ago

    Hello Chris,

    using IronPython, it hasn't changed. You're stuck with VS2019, which is hard to find nowadays. I've added the setup to my Dropbox OneDrive folder.

    https://www.dropbox.com/scl/fo/7j7skk1rou5ixjjqm216x/AKXgwLyfxRc2nVHrPdgfaZY?rlkey=7ghdt7c7a3gvzjwm3ksynzmbi&st=oot1c1l2&dl=0

    https://1drv.ms/f/c/e8b766b43a916613/IgAUaLqLzWoiR78RKwJlyQtCAfYw06lH5qxhZcwse--9cPU?e=ky98bu

    There you'll also find some videos on how to setup VS and some simple training videos. If you use my Training VS solution you're more or less ready to go.

    I'm pretty sure I did have a video/GIF showing the VS, Ironpython setup as well. I'll search for it. Check the dropbox later again.

    Edit: Looks as if I only had some screenshots.

    I've also uploaded the old Trimble sample macros there, since they have disappeared from the Macro forum blog pages. They won't run in newer TBC versions, but I unzipped them somewhere else and always have them open in Notepad++.

    My public GitHub contains most of my Macros to this date, close to 100 I believe:

    https://github.com/RonnySchneider/SCR_Macros_Public

    old forum posts about getting started

    https://community.trimble.com/question/june-2024-installation-for-macros

    is referring to even older

    https://community.trimble.com/communities/community-homepage/digestviewer/viewthread?GroupId=415&MessageKey=4fa77769-d955-4525-80ff-daca0707c274&CommunityKey=8a262af4-a35e-4e9a-9dd3-191cc785899a

    With C++ and C# you'll be able to use the latest VS version. But I'm unfamiliar with using C++ or C# for TBC macro programming and how you debug them properly, including hotloading etc..

    Something I need to look into since IronPython support won't get any better in the future.

    Don't expect to find any decent documentation. 8 years later and all Trimble provides are the lightly documented assemblies in the SDK, which you can browse and search with the VS object browser.

    Edit: I haven't seen a new SDK for 2025 and higher yet; the latest one here on the forum is still only for 2024.00; for a while there was one that had a typo in the file name but was supposedly for 2024.10; I do still have that one, it's in my TBC installation collection, see below.

    Since occasionally somebody asks for an older TBC version, and Trimble for some inexplicable reason doesn't provide them here anymore, I've recently uploaded all the versions that I've collected in the last 10 years to my OneDrive. Quite ridiculous, paying maintenance fees and still doing the archiving for them.

    https://1drv.ms/u/c/e8b766b43a916613/IQB46pPQagPnSpGgiYEBKNE3AcZzf9Mt-e8yQpi-jgZ-8u4?e=v6BSLX

    I'll probably move the training videos there as well, I totally forgot that I have 1TB of storage space on OneDrive. I'll update the link above when I do.



    ------------------------------
    Ronny Schneider
    ------------------------------



  • 3.  RE: TML Development Environment for TBC 2025.21

    Posted 23 days ago

    Much appreciated Ronnie!!!  Thankyou for sharing back to this little TML community.



    ------------------------------
    Chris Siebern
    ------------------------------



  • 4.  RE: TML Development Environment for TBC 2025.21

    Posted 21 days ago

    Hello Chris,

    My understanding is that VS2019 is the latest Visual Studio version that (still) supports IronPython (for IntelliSense and debugging).

    Here are links to the Trimble downloads for Macros SDK 2024.00 and 2024.10. (I just verified that they start a download)

    https://downloads.trimblegeospatial.com/tbc/macrossdk/trimble_macros_sdk_v2024.00.msi

    https://tbcrelease.blob.core.windows.net/download/trimble_macros_sdk_v2040.10.msi

    (You can rename the MSI file if you want, I don't think it matters for installation.)

    The only training material I've heard of are the handful of webpages on this Community website, one presentation at Trimble Dimensions 2018, and the screenshots and videos Ronny has provided.

    As mentioned in my first DevLog 001, I create TBC "extension commands", which are (basically) entirely implemented in C#.  This allows you to use any Visual Studio version that supports .NET Framework 4.8, with all the IDE features (including AI).

    I don't have formal training created for this style of development (yet), but would be happy to show you how it works if you're interested.  Providing virtual and in-person training for extension commands (and everything I know about the TBC API...) are on the future roadmap, but right now, the best I can do is show you what I'm doing myself.

    You mentioned checking these forums weekly - are you just getting into macro development, or have you been playing with it for some time?  Do you have previous development experience?



    ------------------------------
    Quan Mueller
    Revenant Solutions | TBC Extension Developer
    Superuser Program | superuser@revenantsolutions.com
    ------------------------------



  • 5.  RE: TML Development Environment for TBC 2025.21

    Posted 15 days ago
    Edited by David Brubacher 15 days ago

    I've chosen to abandon the python route except for a 10-line shim and do everything in C# like @Quan Mueller. It works very well and I can use VS 2026 and AI to help navigate the sparsely documented API. Here's some stuff out of my Claude file that might help

    ### Media / Photos
    - Get the container: `FilePropertiesContainer.ProvideFilePropertiesContainer(project)`
    - Enumerate: `foreach (var item in container)` - check if each item is `FileProperties`
    - Key properties on `FileProperties`:
      - `fp.FilePath` - current file path (readable/writable); use for relative paths (e.g., `"..\\Photos\\filename.jpg"`)
      - `fp.FullName` - absolute file path (readable/writable); set this to update the media link
      - `fp.Name` - file name only
    - Check if an object has media: `obj.FileProperties.Count > 0`
    - Objects with media (e.g., points with photos) expose `.FileProperties` collection; iterate with `for fp in obj`
    - `MediaFolder` / `MediaFolderContainer` types exist in the API but are NOT how photos are discovered or updated - use `FilePropertiesContainer` instead
    - Photos observe points via Concordance: `GetObserversOf(point.SerialNumber)` may include `FileProperties` or related objects
    
    ### Feature Code Observations
    - Create via `new FeatureCodeObservation(project)`, then `Populate(point)`
    - `RawString` gives the current feature code string
    - `FeatureNodeList` contains parsed codes with their `Attributes`
    - To update: `UpdateFeatureCode(code)` then `UpdateRawFeature()`
    
    ### Concordance (object relationships)
    - `GetObserversOf(serial)` - who is watching this object
    - `GetIsObservedBy(serial)` - what this object watches
    - `Lookup(serial)` - resolve serial to object
    
    ### Points
    - Use `point.FullDescription1` / `point.FullDescription2` (not `.Description1`/`.Description2`)
    - `point.FeatureCode` - the raw feature code string
    - `point.IncludeInSurface` - unreliable after recompute
    - `point.SerialNumber` - unique identifier
    - `point.PointID` - the point name/ID (readable/writable)
    - `point.AnchorName` - alternate way to read the point name (used in Concordance lookups)
    - `point.Position` - `Point3D` with `.X` (Easting), `.Y` (Northing), `.Z` (Elevation)
    - `point.Layer` - layer serial number (readable/writable)
    
    ### SnapIn Discovery
    - TBC project implements `IEnumerable<ISnapIn>` - iterate `foreach (ISnapIn snapIn in project)` to find containers
    - Use `TbcSnapInHelper.TryFindFirstSnapIn(typeof(T), out var result)` to locate specific snap-in types
    - Key snap-ins: `PointManager` (from `Trimble.Vce.Data.RawData`), `FeatureCodeManager`, `CSDContainer` (coordinate system)
    - `FeatureCodeManager.Provide(project)` returns the manager - do NOT cache; re-create each time (enumeration unreliable when reused)
    - `FeatureCodeManager` enumeration may not return all codes from the FXL; supplement with known codes manually
    
    ### PointManager
    - Find via snap-in enumeration: `foreach (var o in project) { if (o is PointManager pm) ... }`
    - `pm.AssociatedRDFeatures(point.SerialNumber)` - returns feature code observations linked to a point
    - Each returned feature has `.Code` (readable/writable) - can rename feature codes this way
    
    ### Feature Code Manager
    - Iterate with `foreach (var fc in featureCodeManager)` - each has `.Code`, `.Type` (GeometryTypes enum)
    - `GeometryTypes.Point`, `GeometryTypes.Block`, `GeometryTypes.Line` - filter by type
    - Code definitions come from the project's FXL (Feature Library) file
    - Attributes on codes: access via `FeatureCodeObservation.FeatureNodeList[i].Attributes`
    - Each attribute has `.Name`, `.Value`, `.AttributeType` (Double/String/Integer/List), `.Definition`
    
    ### Layers
    - Access layer collection: `project.GetMemberContainer(MandatoryContainers.LayerCollection) as LayerCollection`
    - Also via fixed serial: `project[Project.FixedSerial.LayerContainer]`
    - Find layer: iterate `foreach (Layer layer in layerContainer)`, compare `layer.Name`
    - Create layer: `layerContainer.Add<Layer>()` then set `newLayer.Name`
    - Assign to object: `obj.Layer = layer.SerialNumber`
    - Layer members: `layer.Members` - returns serial number list of all elements on that layer
    - Verify layer exists: `Concordance.Lookup(serial)` then check `obj.GetSite() is LayerCollection`
    
    ### Linestrings / Lines
    - Create: `var ls = container.Add(typeof(Linestring))` where container is usually WorldView
    - Get container for view: `ViewHelper.GetContainerForView(clickWindow)` - usually WorldView
    - Add elements: `ElementFactory.Create(typeof(IStraightSegment), typeof(IXYZLocation))` then `ls.AppendElement(e)`
    - For point-based linestrings: use `IPointIdLocation` instead of `IXYZLocation`, set `e.LocationSerialNumber = pointSerial`
    - Set properties: `ls.Name`, `ls.Layer`, `ls.Color`, `ls.LineStyle`, `ls.LineTypeScale`, `ls.Weight`
    - Close linestring: `ls.Closed = true` (if object implements `ILinestringModifier`)
    - Copy from existing geometry: `ls.Append(sourceObject, false, false)` or `ls.Append(polySeg, null, true, false)`
    - Delete old object after conversion: `project.ReplaceEntity(oldSerial, newSerialArray)`
    
    ### Object Properties (common to most graphical objects)
    - `obj.Layer` - layer serial number (readable/writable)
    - `obj.Color` - color (readable/writable)
    - `obj.Weight` - line weight (readable/writable)
    - `obj.LineStyle` - line style serial number (readable/writable)
    - `obj.LineTypeScale` - line type scale factor (readable/writable)
    - `obj.Name` - object name; for some types use `IName.Name` interface instead
    - `obj.GetSite()` - returns the container the object lives in (WorldView, LayerCollection, etc.)
    ### Settings / Options Persistence
    - **Project-scoped persistence**: Use `ConstructionCommandsSettings.ProvideObject(project)` - this is the preferred and proven mechanism for storing custom key-value data in the TBC project file.
      - `settings.GetString(key)` / `settings.SetString(key, value)`
      - `settings.GetBoolean(key)` / `settings.SetBoolean(key, value)`
    ### Known API Quirks - `point.Description1`/`.Description2` return empty - use `point.FullDescription1`/`.FullDescription2` - `point.IncludeInSurface` resets to feature library default on project recompute - `FeatureCodeManager` enumeration is unreliable - may miss codes defined in the FXL - `AssociatedNotes(pointSerial)` only returns point-linked notes, not standalone job notes - Symbols cannot rotate directly - rotation must be tied to an attribute (which has bugs) - TBC version differences: `Trimble.Sdk` (v5.50+) vs older `Trimble.Vce.*` assemblies - use try/except pattern - `MediaFolderContainer` snap-in only exists when photos have been imported into the project - Points with no feature code still appear in `PointCollection` - always check `string.IsNullOrEmpty(point.FeatureCode)` before processing - `TransactionManager.AddEndMark` MUST be called in finally block - if missed, TBC becomes unresponsive and requires restart - Hold serial numbers instead of object references - safer across transaction boundaries - `obj.GetSite()` returns the container - use `instanceof LayerCollection` / `PointCollection` to determine object category   - `settings.GetUInt32(key)` / `settings.SetUInt32(key, value)` - `settings.KeyExists(key)` - check before reading to distinguish "not set" from "empty" - Supported types: `string`, `int` (Int32), `uint` (UInt32), `double`, `bool` only - **Long, float, DateTime, Guid, arrays, collections, and null are NOT supported.** - Data is stored in `UserDefinedAttributes` (serial 27) against `ConstructionCommandsSettings` (serial 1135) and persists across project reopen/recompute - Use key format `MTE_{MacroName}.{SettingName}` (e.g., `MTE_ProjectProperties.OfficeName`) - **App-wide persistence**: Use `OptionsManager` for settings that are NOT project-specific (stored in Options.bin, shared across all projects) - use it only for app-wide macro settings, NOT for project-scoped data. - `OptionsManager.GetBool(key, default)`, `GetString`, `GetInt`, `GetDouble`, `GetUint` - `OptionsManager.SetValue(key, value)` - **WARNING**: `UserInfoCollection` custom keys do NOT reliably persist in TBC macros. Do not use `UserInfoCollection["UniqueID_*"]` as a persistence mechanism. The Fax field encoding technique (OfficeUserFax, FieldOperatorFax) was a workaround and is now superseded by `ConstructionCommandsSettings`. - **WARNING**: Encoding custom data into `IProjectUserInfo` Fax fields (`OfficeUserFax`, `FieldOperatorFax`, `CompanyFax`) is a legacy workaround - use `ConstructionCommandsSettings` instead. Standard `IProjectUserInfo` fields (name, email, phone, address) may still be written for TBC UI compatibility. ### Units - `project.Units.Linear` - linear distance units - `project.Units.Station` - chainage/station units - `units.Convert(fromType, value, toType)` - convert between unit types - `units.DisplayType` - current display unit type - `units.Properties.Copy()` - copy format properties (then set `.AddSuffix = false` for raw numbers) - `units.Format(value, formatProperties)` - format a value as string ### Surfaces (Model3D / DTM) - Select surface: use `SurfaceTypeLists.AllWithCutFillMap` as entity filter - Key properties: `surface.NumberOfTriangles`, `surface.NumberOfVertices` - `surface.GetVertexPoint(vertexIndex)` - returns `Point3D` - `surface.IsVertexPresent(vertexIndex)` - check if vertex exists - `surface.GetTriangleIVertex(triangleIndex, side)` - get vertex index for triangle corner - `surface.GetTriangleVertex(triangleIndex, side)` - returns `(vertexIndex, Point3D)` tuple - `surface.GetTriangleMaterial(index)` vs `surface.NullMaterialIndex()` - check if triangle is visible - `surface.GetTriangleOuterSide(triangle, side)` - check if edge is on surface boundary - `surface.PickSurface(point3d)` - returns `(found, surfacePoint)` for point-on-surface queries ### Selection and Validation - Selection callback: `objs.IsEntityValidCallback = IsValid` - filter what can be selected - `SelectionContextMenuHandler` - customize right-click context menu - `optionMenu.ExcludedCommands = "SelectObservations | SelectPoints | SelectDuplicatePoints"` - Get selected entities: `objs.SelectedMembers(project)` - returns enumerable of selected objects - To array: `Array[ISnapIn](objs.SelectedMembers(project))` - Global selection: `GlobalSelection.Clear()` to deselect all ### WorldView and Containers - Get WorldView: `project[Project.FixedSerial.WorldView] as WorldView` - Get block collection: `project[Project.FixedSerial.BlockCollection]` - Get layer container: `project[Project.FixedSerial.LayerContainer]` - Add objects: `worldView.Add(typeof(Linestring))`, `container.Add(typeof(T))` - Remove objects: `container.Remove(serialNumber)` - List objects: use `IMemberManagement` interface; check `IRepresentGraphically` for graphical objects - Plan set sheets: `PlanSetSheetView.GetPlanSetContainer(project)` ### ModelEvents (entity change notifications) - `ModelEvents.EntityEdited += handler` - fires when an entity is modified - `ModelEvents.EntityDeleted += handler` - fires when an entity is deleted - Event args: `eArgs.EntitySerialNumber` - serial of the affected entity - Always unsubscribe in `Dispose`: `ModelEvents.EntityEdited -= handler` ### User Attributes - Access via `SnapInAttributeExtension.UserAttributes.Overloads[ISnapIn](selectedObject)` - Returns dictionary of key-value pairs - Not available on the project object itself (`Project.FixedSerial.Project`) ### Object Inspection (for diagnostic tools) - Type info: `obj.GetType()`, walk `.BaseType` for inheritance chain - Interfaces: `obj.GetType().GetInterfaces()` - Check `IName`: `if (obj is IName named) { named.Name }` - Concordance observers/observed: see Concordance section above ### ForeignCad Types (imported CAD objects) - `PolyLine`, `PolyLineBase`, `Poly3D` - polyline types - `Arc` (geometry), `Circle` - curve types - `CadPoint` - CAD point (different from survey `Point`) - `MText`, `Text` (CadText) - text entities - `BlockReference` - block insert - `Leader` - leader line - `Hatch` - hatch pattern - All have `.Layer`, `.Color`, `.Weight` properties - Text-specific: `.TextStyleSerial`, `.AlignmentPoint`, `.Normal` - UCS handling: `IHaveUcs` interface, `UCSHelper.GetUcsTiltToWorld(obj.Normal)`



    ------------------------------
    David Brubacher
    ------------------------------



  • 6.  RE: TML Development Environment for TBC 2025.21

    Posted 14 days ago

    Why don't you simply provide a skeleton sample macro (or even better VS solution) like my training macro 5 that has a layer picker in it and is drawing a line on the screen and showcases how a C project is structured in terms of GUI and program code interaction, debugging. How does a simple XMAL/PY combination look as C code for immediate use and debugging/hot loading in TBC?

    Throwing "10-line shim" ???? and "Claude file" ????? at us doesn't help and requires extensive research on our end.



    ------------------------------
    Ronny Schneider
    ------------------------------



  • 7.  RE: TML Development Environment for TBC 2025.21

    Posted 14 days ago

    Hi David!

    Thanks for sharing about your process.  Awesome to get your shim file down to 10 lines!  Made me look at my "shim" file, and it's 210 lines (facepalm).

    Though it has lots of blank lines for spacing, comments, etc. and is doing a lot more than just "code as much in C# as we can".  You only have to modify 2 strings and 2 comments at the top to customize it.  But it got my brain grinding on ways to automate/simplify it!

    I'm meeting with my first TBC command student next week, and nothing like training someone on your process to get you to realize how much it needs to be improved.  By the end of my grinding designing, the roadmap is to not need a Python file at all... in a nutshell, it'll be built/signed/deployed from the C# assembly that has the "command" class and the "command UI" class.  A distraction from today's original goal, but a great improvement - thanks for inadvertantly challenging me!

    And thanks for the Claude markdown file contents (I saved it to my desktop right away!). That gives me a great example of what items people are interested in and using (TBC's API is massive), and a great example of a markdown file.  I've only dabbled a few times with Copilot in VS while doing some web development last year.  But now I've added "sample AI markdown files" to my training idea list.

    Keep up the great work!



    ------------------------------
    Quan Mueller
    Revenant Solutions | TBC Extension Developer
    Superuser Program | superuser@revenantsolutions.com
    ------------------------------



  • 8.  RE: TML Development Environment for TBC 2025.21

    Posted 14 days ago

    Honestly,

    stop the sugarcoating, he didn't share anything about his process. Knowing that he needs 10 lines of code and you 200 doesn't help anybody, without knowing how the code looks like.

    When it comes to programming in C instead of IronPython for TBC I always just read here "I know how to do it", but without providing any decent information.

    Either spill the beans and post something properly helpful or just don't post at all.

    And keep advertising to pay for services out of the forum, you can contact people privately.



    ------------------------------
    Ronny Schneider
    ------------------------------



  • 9.  RE: TML Development Environment for TBC 2025.21

    Posted 14 days ago

    Quan,
    I understand what you are going through. All of my work has company info in it so I can't just copy and paste. I am putting together a sample solution I will share on Github that implements MVVM with a rich, TBC specific  ViewModelBase, guidance around setting up a dev environment on a prod machine, a deployment script, a macro demonstrating a modal dialog with some common scaffolding, the same for a non-modal pane, my diagnostic macro that I use for API discovery and a unit testing project with tests for all the code. Since the '10-line shim' line seems to have attracted attention, here it is. The total file, with comments, is 24 lines, most of which is setup. Execute is only 5 lines. Not that this is for a modal dialog. Non-modal is by necessity longer, but 50 to 60 lines is common. None of it is challenging code. This is just a file in the solution - no need to have any python support.

    # ExportToCivil.py - IronPython launcher for the Export to Civil 3D macro.
    # This is a minimal shim: all logic lives in the C# library (MyMacroLib.dll).
    # TBC loads this file, calls Setup() to register the command, then Execute() to run it.
    import clr
    clr.AddReference("System.Drawing")
    from System.Drawing import Bitmap
    
    def Setup(cmdData, macroFileFolder):
        cmdData.Key = "ExportToCivil"
        cmdData.CommandName = "ExportToCivil"
        cmdData.Caption = "Export data to Civil 3D"
        try:
            cmdData.Version = 1.5
            cmdData.MacroAuthor = "David M. Brubacher"
            cmdData.MacroInfo = "This Macro exports points and meta-data for seamless import to Civil3D"
        except: pass
        try: cmdData.ImageSmall = Bitmap(macroFileFolder + "\\" + cmdData.Key + ".png")
        except: pass
    
    def Execute(cmd, currentProject, macroFolder, parameters):
        clr.AddReferenceToFileAndPath("C:\\ProgramData\\Trimble\\MacroCommands3\\MyMacros\\MyMacroLib.dll")
        from Db.Tbc import MacroLauncher
        launcher = MacroLauncher(currentProject, macroFolder)
        launcher.ExportToCivil()


    ------------------------------
    David Brubacher
    ------------------------------



  • 10.  RE: TML Development Environment for TBC 2025.21

    Posted 14 days ago
    Edited by David Brubacher 14 days ago

    Here is MacroLauncher(), since it plays a role in shortening the .py file but also makes launching consistent and easy.

    using System.Windows;
    using Db.Tbc.Export;
    using Db.Tbc.Support;
    
    using Trimble.Vce.Core.Components;
    
    namespace Db.Tbc
    {
        /// <summary>
        ///     Entry point for launching TBC macros from IronPython shims.
        ///     Each macro's .py Execute() creates a MacroLauncher and calls the
        ///     appropriate method. All methods follow the same pattern:
        ///     create View -> extract ViewModel from DataContext -> Initialize -> ShowDialog.
        /// </summary>
        public class MacroLauncher
        {
            private readonly Project _project;
            private readonly string _macroDirectory;
    
            public MacroLauncher(Project project, string macroDirectory)
            {
                _project = project;
                _macroDirectory = macroDirectory;
            }
    
            /// <summary>
            ///     Launch a modal dialog macro. The View must set its DataContext to the ViewModel in XAML.
            /// </summary>
            /// <typeparam name="TView">WPF Window type (must have parameterless constructor)</typeparam>
            /// <typeparam name="TViewModel">ViewModel type inheriting MyMacroLibBase</typeparam>
            private void LaunchModal<TView, TViewModel>()
                where TView : Window, new()
                where TViewModel : MteMacroLibBase
            {
                var view = new TView();
                if (!(view.DataContext is TViewModel viewModel))
                    return;
                viewModel.Initialize(_project, _macroDirectory);
                view.ShowInTaskbar = false;
                view.ShowDialog();
            }
    
            public void ExportToCivil()
                => LaunchModal<ExportToCivilView, ExportToCivilViewModel>();
    
            // Other modal macros...
    
            public void RunDiagnostic()
                => LaunchModal<TbcDiagnosticView, TbcDiagnosticViewModel>();
        }
    }
    



    ------------------------------
    David Brubacher
    ------------------------------