Table of Contents
C# Connecting and Managing a Link to the CATIA App
In the past we have used the ‘GetObject’ or ‘CreateObject’ methods in VBA to create a COM connection to CATIA V5 and V6. With the introduction of .NET this method changed a little to use the Marshalling class but was essentially the same. Now with the introduction of .NET Core we no longer have a Marshalling class to rely on.
Custom Marshalling Class
So with a little trawling around I was able to find a custom Marshalling class that we can use within .NET Core.
public static class CustomMarshal { internal const String OLEAUT32 = "oleaut32.dll"; internal const String OLE32 = "ole32.dll"; [System.Security.SecurityCritical] // auto-generated_required public static Object GetActiveObject(String progID) { Object? obj = null; Guid clsid; // Call CLSIDFromProgIDEx first then fall back on CLSIDFromProgID if // CLSIDFromProgIDEx doesn't exist. try { CLSIDFromProgIDEx(progID, out clsid); } // catch catch (Exception) { CLSIDFromProgID(progID, out clsid); } GetActiveObject(ref clsid, IntPtr.Zero, out obj); return obj; } //[DllImport(Microsoft.Win32.Win32Native.OLE32, PreserveSig = false)] [DllImport(OLE32, PreserveSig = false)] [ResourceExposure(ResourceScope.None)] [SuppressUnmanagedCodeSecurity] [System.Security.SecurityCritical] // auto-generated private static extern void CLSIDFromProgIDEx([MarshalAs(UnmanagedType.LPWStr)] String progId, out Guid clsid); //[DllImport(Microsoft.Win32.Win32Native.OLE32, PreserveSig = false)] [DllImport(OLE32, PreserveSig = false)] [ResourceExposure(ResourceScope.None)] [SuppressUnmanagedCodeSecurity] [System.Security.SecurityCritical] // auto-generated private static extern void CLSIDFromProgID([MarshalAs(UnmanagedType.LPWStr)] String progId, out Guid clsid); //[DllImport(Microsoft.Win32.Win32Native.OLEAUT32, PreserveSig = false)] [DllImport(OLEAUT32, PreserveSig = false)] [ResourceExposure(ResourceScope.None)] [SuppressUnmanagedCodeSecurity] [System.Security.SecurityCritical] // auto-generated private static extern void GetActiveObject(ref Guid rclsid, IntPtr reserved, [MarshalAs(UnmanagedType.Interface)] out Object ppunk); }
CATIAV6Connection
In the past the connection class has been static, meaning that if there was an active CATIA session then you would get the Application object. If the CATIA application closed then your application would not know. Similarly if your application started first and then CATIA was started your application would not know.
To improve on this I’m using a background worker to validate if the CATIA application is still running every 2 seconds.
public class CATIAV6Connection { private BackgroundWorker? _backgroundworker { get;} = null; private int _timeSpan { get; } = 2000; public bool Connected { get; private set; } = false; public Application? CATIAV6App { get; private set; } = null; public Editor? CATIAV6ActiveEditor { get; private set; } = null; public event EventHandler? OnConnectedChanged; public CATIAV6Connection(bool iStartConnection = true,int iTimeSpan = 2000) { _timeSpan = iTimeSpan; _backgroundworker = new BackgroundWorker(); _backgroundworker.DoWork += new DoWorkEventHandler(DoWork); _backgroundworker.ProgressChanged += new ProgressChangedEventHandler(ProgressChanged); _backgroundworker.WorkerReportsProgress = true; _backgroundworker.WorkerSupportsCancellation = true; if (iStartConnection == true){ _backgroundworker.RunWorkerAsync(iTimeSpan); } } public void StartBackGroundWorker() { if (_backgroundworker != null) { _backgroundworker.RunWorkerAsync(_timeSpan); } } public void StopBackGroundWorker() { if (_backgroundworker != null) { if (!_backgroundworker.IsBusy) { _backgroundworker.CancelAsync(); _backgroundworker.Dispose(); GC.Collect(); } } } private void ProgressChanged(object? sender, ProgressChangedEventArgs? e) { if (e != null) { if (e.UserState != null) { if (e.UserState is Connection) { Connection connection = (Connection)e.UserState; if (connection.appCATIA!=null) { if(CATIAV6App ==null){ CATIAV6App = connection.appCATIA; Connected = connection.isConnected; OnConnectedChanged?.Invoke(this, EventArgs.Empty); } CATIAV6ActiveEditor = CATIAV6App.ActiveEditor; } else { CATIAV6App = null; Connected = false; OnConnectedChanged?.Invoke(this, EventArgs.Empty); } } } } } private void DoWork(object? sender, DoWorkEventArgs? e) { if (sender != null) { BackgroundWorker? worker = (BackgroundWorker)sender; while (!worker.CancellationPending) { Thread.Sleep(_timeSpan); Application? App = null; if (_backgroundworker != null) { try { App=(Application)CustomMarshal.GetActiveObject("Catia.Application")??null; } catch(Exception ex) { Console.WriteLine(ex.Message); } finally { _backgroundworker.ReportProgress(0, new Connection(App)); } } } } } } internal class Connection { public Application? appCATIA { get; set; } = null; public bool isConnected { get; set; } = false; public Connection(Application? iApp) { if(iApp is Application) { appCATIA = iApp; isConnected = (iApp != null) ? true : false; } } }
View Model
Within the View Model we have to subscribe to the ‘OnConnectedChanged’ Event handler, so when the CATIA application is either opened or closed an event is raised and we can do something within the User Interface. In this case the color of the Ellipse is changed form Red to Green when there is an active CATIA session and Red when not.
public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } private Brush _connectionBrush = Brushes.Red; public Brush ConnectionBrush { get { return _connectionBrush; } set { _connectionBrush = value; OnPropertyChanged(nameof(ConnectionBrush)); } } private CATIAV6Connection _catiaV6Connection = new CATIAV6Connection(); public ViewModel() { _catiaV6Connection.OnConnectedChanged += OnConnectedChanged; } private void OnConnectedChanged(object? sender, EventArgs e) { ConnectionBrush = (_catiaV6Connection.Connected == false) ? Brushes.Red : Brushes.Green; } }
WPF XAML UI
Within the User Interface UI we will add an Ellipse and Bind the fill color to the Property in the View Model.
<Window x:Class="TestAppUI.MainWindow" 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:TestAppUI" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Ellipse HorizontalAlignment="Left" Height="109" Margin="10,10,0,0" Stroke="Black" VerticalAlignment="Top" Width="110" Fill="{Binding ConnectionBrush}"/> </Grid> </Window>
Code Behind
Within the Code Behind we will establish the Data Context tot he View Model.
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); DataContext = new ViewModel(); } }
Proof in the Pudding
When the CATIA Application is not running then the Ellipse is Red
And when CATIA is Running then the Ellipse will turn Green
Last Words
This can easily be expanded to work with any Application by changing the String Value in the ‘GetActiveObject’ method and the object type being casted.
App=(Application)CustomMarshal.GetActiveObject("Catia.Application")??null;
WPF Custom Control
Having a simple Ellipse is great but having a reusable control would be nicer, so in this section were going to walk through building a custom control to show if there is an active CATIA session.
Custom Control and Code Behind
The XAML file below lays out the custom control view.
<UserControl x:Class="TestAppUI.CustomControl.CATIAConnectedUserControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="120" d:DesignWidth="70" x:Name="CATIAConnectionUserControl" SizeChanged="CATIAConnectionUserControl_SizeChanged"> <UserControl.Resources> <Style TargetType="Button" x:Key="RoundButton"> <Style.Resources> <Style TargetType="Border"> <Setter Property="CornerRadius" Value="{Binding myBorderRadiusProperty,ElementName=CATIAConnectionUserControl,FallbackValue=28}" /> </Style> </Style.Resources> </Style> </UserControl.Resources> <Border CornerRadius="{Binding BorderRadius,ElementName=CATIAConnectionUserControl,FallbackValue=33}" BorderThickness="{Binding myBorderThicknessProperty,ElementName=CATIAConnectionUserControl,FallbackValue=1}" BorderBrush="{Binding myBorderBrushProperty,ElementName=CATIAConnectionUserControl,FallbackValue=White}"> <Grid> <Ellipse x:Name="GreenLight" Width="{Binding EllipseSize,ElementName=CATIAConnectionUserControl,FallbackValue=66}" Height="{Binding EllipseSize,ElementName=CATIAConnectionUserControl,FallbackValue=66}" Margin="{Binding GreenLightMargin,ElementName=CATIAConnectionUserControl,FallbackValue=1 1 0 0}" HorizontalAlignment="Left" VerticalAlignment="Top" Fill="{Binding CurrentGreenColor,ElementName=CATIAConnectionUserControl,FallbackValue=Green}" Stroke="{Binding myGreenLightBorderProperty,ElementName=CATIAConnectionUserControl,FallbackValue=Green}" StrokeThickness="{Binding myBorderThicknessProperty,ElementName=CATIAConnectionUserControl,FallbackValue=1}"/> <Ellipse x:Name="RedLight" Width="{Binding EllipseSize,ElementName=CATIAConnectionUserControl,FallbackValue=66}" Height="{Binding EllipseSize,ElementName=CATIAConnectionUserControl,FallbackValue=66}" Margin="{Binding GreenLightMargin,ElementName=CATIAConnectionUserControl,FallbackValue=1 0 0 1}" VerticalAlignment="Bottom" HorizontalAlignment="Left" Fill="{Binding CurrentRedColor,ElementName=CATIAConnectionUserControl,FallbackValue=Red}" Stroke="{Binding myRedLightBorderProperty,ElementName=CATIAConnectionUserControl,FallbackValue=Red}" StrokeThickness="{Binding myBorderThicknessProperty,ElementName=CATIAConnectionUserControl,FallbackValue=1}"/> </Grid> </Border> </UserControl>
Below is the code behind the custom control view, were still leveraging the CATIAV6Connection class previously created.
using System; using System.ComponentModel; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using TestAppUI.Helpers; namespace TestAppUI.CustomControl { /// <summary> /// Interaction logic for CATIAConnectedUserControl.xaml /// </summary> public partial class CATIAConnectedUserControl : UserControl, INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public int myBorderThicknessProperty { get { return (int)GetValue(myBorderThicknessPropertyProperty); } set { SetValue(myBorderThicknessPropertyProperty, value); } } // Using a DependencyProperty as the backing store for myBorderThicknessProperty. This enables animation, styling, binding, etc... public static readonly DependencyProperty myBorderThicknessPropertyProperty = DependencyProperty.Register("myBorderThicknessProperty", typeof(int), typeof(CATIAConnectedUserControl), new PropertyMetadata(1)); public Brush myBorderBrushProperty { get { return (Brush)GetValue(myBorderBrushPropertyProperty); } set { SetValue(myBorderBrushPropertyProperty, value); } } // Using a DependencyProperty as the backing store for myBorderBrushProperty. This enables animation, styling, binding, etc... public static readonly DependencyProperty myBorderBrushPropertyProperty = DependencyProperty.Register("myBorderBrushProperty", typeof(Brush), typeof(CATIAConnectedUserControl), new PropertyMetadata(Brushes.White)); public Brush myRedLightColorProperty { get { return (Brush)GetValue(myRedLightColorPropertyProperty); } set { SetValue(myRedLightColorPropertyProperty, value); } } // Using a DependencyProperty as the backing store for myRedLightColorProperty. This enables animation, styling, binding, etc... public static readonly DependencyProperty myRedLightColorPropertyProperty = DependencyProperty.Register("myRedLightColorProperty", typeof(Brush), typeof(CATIAConnectedUserControl), new PropertyMetadata(Brushes.Red)); public Brush myRedLightBorderProperty { get { return (Brush)GetValue(myRedLightBorderPropertyProperty); } set { SetValue(myRedLightBorderPropertyProperty, value); } } // Using a DependencyProperty as the backing store for myRedLightBorderProperty. This enables animation, styling, binding, etc... public static readonly DependencyProperty myRedLightBorderPropertyProperty = DependencyProperty.Register("myRedLightBorderProperty", typeof(Brush), typeof(CATIAConnectedUserControl), new PropertyMetadata(Brushes.Green)); public Brush myGreenLightColorProperty { get { return (Brush)GetValue(myGreenLightColorPropertyProperty); } set { SetValue(myGreenLightColorPropertyProperty, value); } } // Using a DependencyProperty as the backing store for myGreenLightColorProperty. This enables animation, styling, binding, etc... public static readonly DependencyProperty myGreenLightColorPropertyProperty = DependencyProperty.Register("myGreenLightColorProperty", typeof(Brush), typeof(CATIAConnectedUserControl), new PropertyMetadata(Brushes.Green)); public Brush myGreenLightBorderProperty { get { return (Brush)GetValue(myGreenLightBorderPropertyProperty); } set { SetValue(myGreenLightBorderPropertyProperty, value); } } // Using a DependencyProperty as the backing store for myGreenLightBorderProperty. This enables animation, styling, binding, etc... public static readonly DependencyProperty myGreenLightBorderPropertyProperty = DependencyProperty.Register("myGreenLightBorderProperty", typeof(Brush), typeof(CATIAConnectedUserControl), new PropertyMetadata(Brushes.Green)); public static readonly RoutedEvent SettingConfirmedEvent = EventManager.RegisterRoutedEvent("SettingConfirmedEvent", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(CATIAConnectedUserControl)); public event RoutedEventHandler SettingConfirmed { add { AddHandler(SettingConfirmedEvent, value); } remove { RemoveHandler(SettingConfirmedEvent, value); } } public CATIAV6Connection CATIAV6Connection = new CATIAV6Connection(); public CATIAConnectedUserControl() { InitializeComponent(); CATIAV6Connection.OnConnectedChanged += OnConnectedChanged; } private Brush _currentGreenColor = Brushes.Green; public Brush CurrentGreenColor { get { return _currentGreenColor; } set { _currentGreenColor = value; OnPropertyChanged(nameof(CurrentGreenColor)); } } private Brush _currentRedColor = Brushes.Red; public Brush CurrentRedColor { get { return _currentRedColor; } set { _currentRedColor = value; OnPropertyChanged(nameof(CurrentRedColor)); } } private int _borderRadius = 28; public int BorderRadius { get { return _borderRadius; } set { _borderRadius = value; OnPropertyChanged(nameof(BorderRadius)); } } private double _elipseSize = 16; public double EllipseSize { get { return _elipseSize; } set { _elipseSize = value; OnPropertyChanged(nameof(EllipseSize)); } } private Thickness _greenLightMargin = new Thickness(); public Thickness GreenLightMargin { get { return _greenLightMargin; } set { _greenLightMargin = value; OnPropertyChanged(nameof(GreenLightMargin)); } } private void CATIAConnectionUserControl_SizeChanged(object sender, SizeChangedEventArgs e) { EllipseSize=(this.ActualWidth-(this.myBorderThicknessProperty*2)); GreenLightMargin = new Thickness(left: 0, top: 0, right: 0, bottom: 0); BorderRadius = (int)((this.ActualWidth - (this.myBorderThicknessProperty * 2))/2); } private void OnConnectedChanged(object? sender, EventArgs e) { CurrentGreenColor = (CATIAV6Connection.Connected == true) ? myGreenLightColorProperty : Brushes.Black; CurrentRedColor = (CATIAV6Connection.Connected == false) ? myRedLightColorProperty : Brushes.Black; RaiseEvent(new RoutedEventArgs(SettingConfirmedEvent)); } } }
Main Window XAML and Code Behind
The XAML below shows an implementation of the custom control.
<Window x:Class="TestAppUI.MainWindow" 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:TestAppUI" xmlns:customcontrol="clr-namespace:TestAppUI.CustomControl" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid Background="Black"> <customcontrol:CATIAConnectedUserControl SettingConfirmed="CATIAConnectedUserControl_SettingConfirmed" VerticalAlignment="Top" HorizontalAlignment="Left" myBorderThicknessProperty="2" myBorderBrushProperty="White" myGreenLightColorProperty="Green" myGreenLightBorderProperty="DarkGreen" myRedLightColorProperty="red" myRedLightBorderProperty="DarkRed" Height="100" Width="60" Margin="10 10 0 0"/> </Grid> </Window>
Its important to capture the event that fires when CATIA is opened or closed and then update the CATIA application object in the view model.
using System.Windows; using TestAppUI.CustomControl; namespace TestAppUI { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private ViewModel _viewModel = new ViewModel(); public MainWindow() { InitializeComponent(); DataContext = _viewModel; } private void CATIAConnectedUserControl_SettingConfirmed(object sender, RoutedEventArgs e) { if(sender is CATIAConnectedUserControl) { CATIAConnectedUserControl catiaConnectedUserControl = (CATIAConnectedUserControl)sender; _viewModel.CATApp = catiaConnectedUserControl.CATIAV6Connection.CATIAV6App; } } } }
View Model
Within the View model well create the CATIA Application object property that will either be null or defined based on CATIA being opened or not.
using INFITF; using System.ComponentModel; namespace TestAppUI { public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } public Application? CATApp { get; set; } = null; } }