TBC Macros and Extensions

 View Only
Expand all | Collapse all

Has anyone had any luck using a ResourceDictionary in the loose XAML file?

  • 1.  Has anyone had any luck using a ResourceDictionary in the loose XAML file?

    Posted 05-21-2025 12:15
    Edited by David Brubacher 05-21-2025 12:20

    I can create styles in the <StackPanel.Resources /> section and use the styles on my controls - easy! But with lots of macros and UIs I want a consistent look and feel. Normally I would build a resource dictionary and refer to those in my UI by doing something like

    <StackPanel.Resources>
         <ResourceDictionary Source="SomeFile.xaml" />
    </StackPanel.Resources>
    

    VS complains that "the property 'Source' was not found in the type 'ResourceDictionary'" and intellisense agrees, but it's there. Anyone who has built WPF apps knows it is!

    Then it occurred to me that Source might not be supported in loose xaml, which got me wondering if I could load my UIs from the dll with something from the Trimble.Vce.UI.Wpf namespace (Trimble.Vce.UI.Wpf.Views.MacroUIUserControl looks promising), meaning the xaml is no longer 'loose' and ResourceDictionary, and a few other loose xaml pain points, like setting the data context at design time, would be resolved.

    Has anyone played around with this? It feels like it should be possible and would be closer to the way native TBC is loading macro UI.



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



  • 2.  RE: Has anyone had any luck using a ResourceDictionary in the loose XAML file?

    Posted 05-21-2025 12:30

    I have had this issue in the past and now include the following in all my WPF project. 

    1.) Create a helper class:

    using System.Windows;
    using System.Reflection;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    
    namespace CEDC3DDLPI.Models.Helpers
    {
        static class ResourceHelpers
        {
            public static ResourceDictionary ResolveStylesDictionary<T>()
            {
                ResourceDictionary stylesDictionary = new ResourceDictionary
                {
                    Source = new Uri($"pack://application:,,,/{typeof(T).Assembly.GetName().Name};component/Views/ResourceDictionaryName.xaml")
                };
    
                return stylesDictionary;
            }
    )
    )

    2.) Add the following line to the call of your form and/or and the constructor

    ResourceDictionary stylesDictionary = ResourceHelpers.ResolveStylesDictionary<'formname'>();

    OR:

    1.) call your dictionary like this, and place it at the very beginning of your XAML. Right after the closing > of the window call. Replace 'ASSEMBLYNAME' with your assembly and 'StyleDictionaryName' with your name.

    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/'ASSEMBLYNAME';component/Views/'StylesDictionaryName'.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>


    ------------------------------
    David Prontnicki
    ------------------------------



  • 3.  RE: Has anyone had any luck using a ResourceDictionary in the loose XAML file?

    Posted 05-21-2025 13:27

    Thanks for the fast response, David

    I tried the second - preferable - way already, both as the simple way I posted and as the fully qualified 'pack' style you have. The issue is, I get an exception on Source in the ResourceDictionary regardless of where it is. If this is working for you, then I have a different issue with my project.


    I like the first way except the designer experience is unacceptable.



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



  • 4.  RE: Has anyone had any luck using a ResourceDictionary in the loose XAML file?

    Posted 05-22-2025 14:57

    I think the difference between what you and I are doing is I am creating a StackPanel that plugs into the Command Pane, and you are opening a modal dialog box. I have to rely on wpf.LoadComponent() in python to interpret my XAML and you can simply open the dialog within your called app. Am I understanding that correctly @David Prontnicki?

    For many of my macros I can switch to that method, but I have some coming up that must be in the command pane. I think I'm kicking the can down the road a bit.

    That being said, this problem and your answer prompted a pretty big refactor that should make things easier to maintain and lay the groundwork for bypassing wpf.LoadComponent() in the future. It's nice to have a fully working resource dictionary of styles and a design time data context at the same time (and full intellisense for all those using pure IronPython).

    Also, I've completely done away with the IronPython project in my solution, so perhaps I can switch to VS2022. Or I will realize the error of my ways later and wish I'd never typed something so silly.



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



  • 5.  RE: Has anyone had any luck using a ResourceDictionary in the loose XAML file?

    Posted 05-22-2025 17:00

    @David Brubacher Sorry for the delay in responding. I was traveling. You are correct that process is what I use with my .dll plugins or .exe programs I write with C# and .Net. I am calling modal or modeless dialogs with that. My apologies. I am still trying to wrap my head around developing for Trimble. 

    If you have been able to drop IronPython I would love to learn how. I dont want to install an outdated VS version and feel like I'm going backwards. I am still struggling with the basics of development for Trimble because I dont fully understand the IronPython component and the "load" and debug workflows. So If I can start a VS2022 Python project and work "out of the box" that would be great. If you get it working I would be willing to pay you for a step by step write up. LOL :) 



    ------------------------------
    David Prontnicki
    ------------------------------



  • 6.  RE: Has anyone had any luck using a ResourceDictionary in the loose XAML file?

    Posted 06-05-2025 06:09
    Edited by David Brubacher 06-05-2025 06:34

    OK I have it now.

    For others trying to get this to work, you need a loader method in your UserControl library

        public class MyMacroLauncher
        {
            public MyMacroLauncher(Project project)
            {
                var view = new MyMacroView();
                if (!(view.DataContext is MyMacroViewModel viewModel))
                    return;
                viewModel.Initialize(project);
                view.ShowDialog();
            }
        }

    Then you need a ResourceDictionary in your library with Build Action set to Page

    Next, in your view, reference the ResourceDictionary, set the DataContext and set the Style. Set the style the way I show here. If you set it as an element ahead of the ResourceDictionary, it won't work. If you omit the TargetType it won't work.

    <Window xmlns:wpf="clr-namespace:Trimble.Vce.UI.Controls.Wpf;assembly=Trimble.Vce.UI.Controls" 
            x:Class="MacroLib.MyMacroViewView"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:MyMacro"
            mc:Ignorable="d" Title="My First Macro">
        <Window.Resources>
            <ResourceDictionary>
                <ResourceDictionary.MergedDictionaries>
                    <ResourceDictionary Source="pack://application:,,,/MacroLib;component/MacroStyles.xaml" />
                </ResourceDictionary.MergedDictionaries>
            </ResourceDictionary>
        </Window.Resources>
        <Window.DataContext>
            <local:MyMacroViewModel/>
        </Window.DataContext>
        <Window.Style>
            <Style BasedOn="{StaticResource MacroWindow}" TargetType="{x:Type Window}" />
        </Window.Style>

    You don't need anything in the view code-behind.

    Open you window like this

    def Execute(cmd, currentProject, macroFolder, parameters):
        clr.AddReferenceToFileAndPath("C:\\ProgramData\\Trimble\\MacroCommands3\\MyMacros\\MyMacroLib.dll")
        from MyFirstMacro import MyMacroLauncher
        MyMacroLauncher(currentProject)

    Obviously, this is for windows only. I will post a method to do Command Panes once I get it hammered out.

    With this method you get reuseable styles and code-complete and intellisense in the XAML designer. You should also create a base class for all your macros that implements Trimble.Sdk.UI.ViewModelBase. Here is some of my base class

            /// <summary>
            /// Initialize this class for use with Trimble Business Center
            /// </summary>
            /// <param name="project">A reference to the project to process</param>
            internal virtual void Initialize(Project project)
            {
                // save the project reference
                Project = project;
                // ensure the project file contains the macro-specific options defaults
                SetDefaultOptions();
                // set the macro options to the settings found in the project file
                ApplySettings();
                MacroState = ReadyState.Ready;
            }
    
            /// <summary>
            /// Perform the processing
            /// </summary>
            // ReSharper disable once UnusedMember.Global
            public void DoProcessing()
            {
                MacroState = ReadyState.Processing;
                if (MacroState != ReadyState.Ready)
                    throw new NotSupportedException("The macro is not ready to perform processing.");
                MacroState = ReadyState.Processing;
    
                if (VerboseMessaging)
                {
                    ResultsBuilder.AppendLine($"DEBUG: Transactions are {(SupportTransactions ? "Supported" : "Disabled")}");
                    ResultsBuilder.AppendLine($"DEBUG: Point Manager is {(SupportPointManager ? "Supported" : "Disabled")}");
                    ResultsBuilder.AppendLine($"DEBUG: Feature Code Manager is {(SupportFeatureCodeManager ? "Supported" : "Disabled")}");
                    ResultsBuilder.AppendLine($"DEBUG: UI Events are {(SupportUiEvents ? "Supported" : "Disabled")}");
                    ResultsBuilder.AppendLine($"DEBUG: Command Level Undo is {(SupportUndo ? "Supported" : "Disabled")}");
                    OnPropertyChanged(nameof(Results));
                }
    
                // initialize the managers
                if (SupportPointManager && !SetPointManager())
                    return;
                if (SupportFeatureCodeManager)
                    FeatureCodeManager = FeatureCodeManager.Provide(Project);
    
                try
                {
                    // raise the data processing event
                    if (SupportUiEvents)
                        UIEvents.RaiseBeforeDataProcessing(this, new UIEventArgs());
                    // set an undo mark
                    if (SupportUndo)
                        Project.TransactionManager.AddBeginMark(CommandGranularity.Command, CommandName);
                    // call the macro specific try block either in a transaction or not
                    if (SupportTransactions)
                    {
                        using (Transaction = new TransactMethodCall(Project.TransactionCollector))
                        {
                            // call the custom inner code
                            DoProcessingTryBlock();
                        }
                    }
                    else
                        DoProcessingTryBlock();
    
                }
                catch (Exception e)
                {
                    ResultsBuilder.Append($"ERROR: {e.Message} from {e.Source}");
                    OnPropertyChanged(nameof(Results));
                }
                finally
                {
                    // call the custom cleanup code
                    DoProcessingFinallyBlock();
                    // set an undo mark and close out the UI events
                    if (SupportUndo)
                        Project.TransactionManager.AddEndMark(CommandGranularity.Command);
                    if (SupportUiEvents)
                        UIEvents.RaiseAfterDataProcessing(this, new UIEventArgs());
                    ResultsBuilder.AppendLine();
                    ResultsBuilder.AppendLine("Processing Complete");
                    // Save the macro options to the project file
                    SaveOptions();
                    OnPropertyChanged(nameof(Results));
                }
    
                MacroState = ReadyState.Complete;
            }
    
            /// <summary>
            /// Populate the list of options used by this macro
            /// </summary>
            internal abstract void PopulateMacroOptions();
    
            /// <summary>
            /// Execute the custom code in the try block
            /// </summary>
            protected abstract void DoProcessingTryBlock();
    
            /// <summary>
            /// Execute the custom code in the finally block
            /// </summary>
            protected abstract void DoProcessingFinallyBlock();
    
            /// <summary>
            /// Find the first instance of a snap-in type in the project and return it to the caller.
            /// This method will also return false if the active project has not been set.
            /// </summary>
            /// <param name="snapInToFind">The type of the snap-in to find</param>
            /// <param name="result">The snap-in instance, or null if no instances are found</param>
            /// <returns>True if the snap-in type is found and returned in the out parameter</returns>
            internal bool TryFindFirstSnapIn(Type snapInToFind, out ISnapIn result)
            {
                result = null;
                if (Project == null)
                    return false;
                foreach (ISnapIn snapIn in Project)
                {
                    if (snapInToFind != snapIn.GetType())
                        continue;
                    result = snapIn;
                    return true;
                }
                return false;
            }

    and in the view model, this sets the options like Ronny does in the .py file. 

            /// <summary>
            /// Set up the macro option names, default values and property mappings
            /// </summary>
            internal override void PopulateMacroOptions()
            {
                MacroOptionsBase.Prefix = "MyCompany_MyMacro";
                MacroOptions.Add(new BooleanMacroOption
                {
                    Default = true,
                    Name = "DoSomething",
                    NameOfProperty = nameof(DoSomething)
                });
                MacroOptions.Add(new UIntMacroOption
                {
                    Default = 0,
                    Name = "ReLayerDestinationLayer",
                    NameOfProperty = nameof(ReLayerSerialNumber)
                });
            }
    

    Also, I have my project files where the rest of my Git repos are. I highly recommend using Git so you can have release code and then flip back and forth to your dev code without impacting your real job.

    in your post-build event command line, put this:

    call $(ProjectDir)PostBuildCopy.bat $(TargetDir) $(TargetFileName) $(ProjectDir)


    and in the PostBuildCopy.bat file do this

    @ECHO OFF
    SET baseDir=C:\ProgramData\Trimble\MacroCommands3\MyCompanyMacros
    @ECHO Copying main library
    copy /Y %1%2 %baseDir%
    @ECHO Copying MyFirstMacro resources
    if not exist %baseDir%\MyFirstMacro\ mkdir %baseDir%\MyFirstMacro
    copy /Y %3MyFirstMacro\MyFirstMacro.png %baseDir%\MyFirstMacro
    copy /Y %3MyFirstMacro\MyFirstMacro.py %baseDir%\MyFirstMacro

    This should give you a good start!


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



  • 7.  RE: Has anyone had any luck using a ResourceDictionary in the loose XAML file?

    Posted 06-05-2025 16:35

    Something else to add...

    Add a command line project to your solution. Don't put anything in it but mark it as the startup project.

    Now when you click debug, the project builds, your library and the resources get copied to the macro directory and TBC opens as the debugger for just your library because you added TBC as the debugger for your library.

    I haven't tested this yet, but, if you have Python capability installed in VS, you shouldn't need to start your project as a python project. Just add a simple, stripped down .py files as the resource you copy in the .bat file (per macro) and you should at least get a decent editor experience in VS, but who cares - there aren't many lines in the .py file anyway.

    Also, and this is maybe the best part, you don't need IronPython and can use VS2022.



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