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.
Original Message:
Sent: 06-05-2025 06:08
From: David Brubacher
Subject: Has anyone had any luck using a ResourceDictionary in the loose XAML file?
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 OFFSET baseDir=C:\ProgramData\Trimble\MacroCommands3\MyCompanyMacros@ECHO Copying main librarycopy /Y %1%2 %baseDir%@ECHO Copying MyFirstMacro resourcesif not exist %baseDir%\MyFirstMacro\ mkdir %baseDir%\MyFirstMacrocopy /Y %3MyFirstMacro\MyFirstMacro.png %baseDir%\MyFirstMacrocopy /Y %3MyFirstMacro\MyFirstMacro.py %baseDir%\MyFirstMacro
This should give you a good start!
------------------------------
David Brubacher
Original Message:
Sent: 05-22-2025 16:59
From: David Prontnicki
Subject: Has anyone had any luck using a ResourceDictionary in the loose XAML file?
@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
Original Message:
Sent: 05-22-2025 14:57
From: David Brubacher
Subject: Has anyone had any luck using a ResourceDictionary in the loose XAML file?
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
Original Message:
Sent: 05-21-2025 12:30
From: David Prontnicki
Subject: Has anyone had any luck using a ResourceDictionary in the loose XAML file?
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
Original Message:
Sent: 05-21-2025 12:14
From: David Brubacher
Subject: Has anyone had any luck using a ResourceDictionary in the loose XAML file?
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
------------------------------