Using Prism for building a Plugin (.dll) - wpf

I am developing a plugin to be used for a software I use. The software plugin is attached to the software by attaching the .dll produced from my code. Therefore, the software's documentation requires you to have a certain class (call it CPlugin) as the plugins entry point.
All the tutorials I am reading with Prism is where you initiate your project as a WPF Application. This way your project will have App.xaml and App.cs files where you start to implement your Prism framework. Compiling the code also (for a WPF application) will produce a .exe not a .dll.
The way I have my plugin setup is I start my project with a C# class. I would then create my CPlugin class and initiate all my variables and then display my MainView which creates my ViewModel and take it from there. There is no App.xaml or App.cs. I am not sure how to use Prism with the constraints I have.
This is the software I am developing the plugin for: https://www.csiamerica.com/products/etabs
Upon installation in the install directory; the API helpfile can be found which explains how to develop or initiate a plugin. Here is a sample of the relevant information:
In your plugin, all functionality must be implemented in a class called cPlugin.
Class cPlugin must contain a subroutine cPlugin.Main that has two
reference arguments of types ETABSv1.cSapModel and
ETABSv1.cPluginCallback
Also
Adding a .NET Plugin
The process for adding a .NET plugin is much simpler. In the External Plugin Data form, the user should simply browse for and select the plugin .NET DLL and click the "Add" button
Here is some sample code for a plugin that displays an empty window:
Create a C# Class Library file (.NET Framework), reference the API as one of my references.
CPlugin.cs:
using Combonito.Views; // Access Views (open window)
using CSiAPIv1; //to Access ETABS/SAP2000 API
using System;
using static Globals;
namespace Combonito
{
// cPlugin has to be implemented in ETABS Plugins, it has to contain two functions Main() and Info()
public class cPlugin
{
private MainView _MyForm;
//Entry point of plugin - has to exist with this exact signature
// must call Finish() when plugin is closed!
public void Main(ref cSapModel _SapModel, ref cPluginCallback _ISapPlugin)
{
ParentPluginObject = this;
SapModel = _SapModel;
ISapPlugin = _ISapPlugin;
try
{
_MyForm = new MainView(); //Create MainView
_MyForm.ShowDialog(); // Display window
}
catch (Exception ex)
{
try
{
ISapPlugin.Finish(1);
Console.WriteLine(ex);
}
catch (Exception ex1)
{
Console.WriteLine(ex1);
throw;
}
}
}
// return value should be 0 if successful
public int Info(ref string txt)
{
try
{
txt = "Plugin is written by Moustafa El-sawy (mk.elsawy#live.com)";
}
catch (Exception ex)
{
Console.WriteLine(ex);
throw;
}
return 0;
}
//Deconstructor to clean up
~cPlugin()
{
Console.WriteLine(GC.GetGeneration(0));
}
}
}
I then have an empty window MainView and A viewmodel MainWindowViewModel.
Edit: Here is the boiler initial to start any plugin similar to what I showed above but has more explanation https://kinson.io/post/etabs-plugin-quickstart/

Your starting point should be Bootstrapper
First you need to install:
Prism.Unity
Prism.Wpf
Need to create your bootstrapper based on
https://github.com/PrismLibrary/Prism/blob/master/src/Wpf/Prism.Wpf/PrismBootstrapperBase.cs
Override the virtual method to create Shell (which is your main view that contains regions
Override the virtual method to configure your container.
Finally register your views and viewmodels
P.S.: Consider using an interface for each registered type e.g. IShellViewModel
using Prism;
using Prism.Ioc;
using Prism.Unity;
using System.Windows;
namespace PrismTest
{
public class Bootstrapper : PrismBootstrapperBase
{
protected override IContainerExtension CreateContainerExtension()
{
return new UnityContainerExtension();
}
protected override DependencyObject CreateShell()
{
return Container.Resolve<Shell>();
}
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.Register<IShellViewModel, ShellViewModel>();
}
}
}
Call your Bootstrapper from your plugin :
Bootstrapper bootstrapper = new Bootstrapper();
bootstrapper.Run();
Your View (Shell)
<Window
x:Class="PrismTest.Shell"
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:local="clr-namespace:PrismTest"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="Shell"
Width="800"
Height="450"
mc:Ignorable="d">
<Grid>
<TextBox Text="{Binding MyText}" />
</Grid>
</Window>
In Shell.xaml.cs: Use your view model. This can be also automatically injected
public partial class Shell : Window
{
private readonly IUnityContainer _container;
public Shell(IUnityContainer container)
{
InitializeComponent();
_container = container;
this.DataContext = _container.Resolve<IShellViewModel>();
}
}
Your ViewModel
public class ShellViewModel : BindableBase, IShellViewModel
{
private string m_MyText = "Shell ViewModel Text";
public string MyText
{
get { return m_MyText; }
set { SetProperty(ref m_MyText, value); }
}
}
Your interface:
internal interface IShellViewModel
{
string MyText { get; set; }
}
Resulting view;

Related

CaliburnMicro IEventAggregator won't work with ChildViews

and sorry if this is a somewhat easy question, but I'm still new to CaliburnMicro and EventAggregator is proving to be the worst thing to learn by a long mile. Anyway, let's go to it. I have an app with a main ShellView and a bunch of ChildViews that display various information, and I need certain parameters to be shared between the Views. It did not take very long to find I need to use EventAggregator, but with Net6 I could not make it work, no chance. I found this app someone else did in Net3.1 and CaliburnMicro where a new window is created, on this second window there is a TextBox and a send button. Whatever you type here gets sended to the Main window. I studied the code and replicated the app succesfully, also with the latest version of Caliburn and Net6.
BUT then I decided to modify the app and instead of having a new window, now I have exactly the same but with a ChildView inside the ShellView, and here is where nothing works.
1st on the Bootstrapper
public class Bootstrapper : BootstrapperBase
{
private SimpleContainer _container;
public Bootstrapper()
{
Initialize();
}
protected override void Configure()
{
_container = new SimpleContainer();
_container.Singleton<IWindowManager, WindowManager>();
_container.Singleton<IEventAggregator, EventAggregator>();
_container.PerRequest<ShellViewModel>();
}
protected override object GetInstance(Type service, string key)
{
var instance = _container.GetInstance(service, key);
if (instance != null)
return instance;
throw new InvalidOperationException("Could not locate any instances.");
}
protected override IEnumerable<object> GetAllInstances(Type service)
{
return _container.GetAllInstances(service);
}
protected override void BuildUp(object instance)
{
_container.BuildUp(instance);
}
protected override void OnStartup(object sender, StartupEventArgs e)
{
DisplayRootViewFor<ShellViewModel>();
}
As seen, I've created a SimpleContainer, then initialised it as a Singleton.
Also _container.Singleton<IWindowManager, WindowManager>(); is no longer needed as I am not opening a new window anymore, so that could be commented but as I had so many issues, I let it be just in case for this last try before posting this question.
2nd I've created a class for the message IHandle to manage.
public class EventMessage
{
public string Text { get; set; }
}
3rd I created and edited the ChildViewModel (still called SecondWindowViewModel as this experiment is directly derivated from the original worknig one) and ChildView (well SecondWindowView still). Note that SecondWindowView is a WPF User Control, not a Window.
class SecondWindowViewModel : Screen
{
private readonly IEventAggregator _eventAggregator;
private string _secondTextBox;
public string SecondTextBox
{
get { return _secondTextBox; }
set { _secondTextBox = value; NotifyOfPropertyChange(() => SecondTextBox); }
}
public SecondWindowViewModel(IEventAggregator eventAggregator)
{
this._eventAggregator= eventAggregator;
}
public void SendBack()
{
EventMessage ToSend = new EventMessage();
ToSend.Text = SecondTextBox;
_eventAggregator.PublishOnUIThreadAsync(ToSend);
}
As seen, I have an IEventAggregator _eventAggregator, then on the constructor of the class I added this._eventAggregator= eventAggregator; and then on the method SendBack that is called upon pressing the button I send the message with SubscribeOnUIThread.
And lastly the ShellViewModel:
public class ShellViewModel : Conductor<Object>, IHandle<EventMessage>
{
private readonly IEventAggregator _eventAggregator;
private string _parentText;
public string ParentText
{
get { return _parentText; }
set { _parentText = value; NotifyOfPropertyChange(() => ParentText); }
}
public ShellViewModel(IEventAggregator eventAggregator)
{
ActivateItemAsync(new SecondWindowViewModel(_eventAggregator));
_eventAggregator = eventAggregator;
_eventAggregator.Subscribe(this);
}
public Task HandleAsync(EventMessage message, CancellationToken cancellationToken)
{
ParentText = message.Text;
return Task.CompletedTask;
}
//public void NewWindow()
//{
//WindowManager wm = new WindowManager();
//SecondWindowViewModel swm = new SecondWindowViewModel(_eventAggregator);
//wm.ShowWindowAsync(swm);
//}
}
}
Now here instead of inheriting from Screen and IScreen, I inherit from Conductor because I want to have that ChildView on my form. NewWindow is how it worked before but now that button no longer works as I don't need to launch a new window anymore, that's why it is commented out.
As seen, on the contructor I subscribe _eventAggregator, and then HandleAsync does the job of receiving the message and assign it to a variable. Now on the Caliburn Documentatin the method to use is public void Handle() but that no longer works, that's the only way I managed to make it work.
Now when I run this the app does load and seems to work just fine, but as soon as the SendBack() method gets called (in SecondWindowViewModel) the line _eventAggregator.PublishOnUIThreadAsync(ToSend); launches an exception System.NullReferenceException: 'Object reference not set to an instance of an object.'
From my understanding EventAggregator should not care if I'm sending the message to a Window or a user panel or anything. Only thnig I changed is commenting out NewWindow and deleting the old SecondWindowWiew WPF Window and replacing it with a new SecondWindowView WPF User Control with the exact same XAML, then in ShellWiev added bellow a <ContentControl x:Name="ActiveItem"/>.
I'm a bit of a loss here, I've been trying everything, coping the code from the documentation, looking for tutorials online, other StackOverflow questions, and while I could make the UI load and make HandleAsync work, for 3 days straight I could not make it work with ChildViews. That code I wrote does work with a new window. I even run into problems of ShellView straight up not loading unless putting a new constructor that takes no parameters and empty inside, but that's for another day.
Sorry for the extra long post, but I think it's important to put all the information out there. Thank you for your time and again, sorry if this is somewhat of a dumb question, but we all have to start somewhere no?

How to execute a Revit IExternalCommand from a WPF button?

I am in need of help.
I have created a dockable WPF within Revit.
It is working well and I can 'show' & ;hide' from push buttons.
My aim is to create buttons within the WPF that run custom commands.I dont need to interact or show any information within the WPF, its purely just acting as a push button but in the WPF instead of a ribbon.
The commands currently work and can be executed via the Add-In Manager.
Below is the command I am trying to run:
using Autodesk.Revit.Attributes;
using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using Autodesk.Revit.UI.Selection;
using System.Collections.Generic;
using System.Linq;
namespace Adams.Commands
{
[Transaction(TransactionMode.Manual)]
[Regeneration(RegenerationOption.Manual)]
public class PrecastDisallowJoin : IExternalCommand
{
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
var uiApplication = commandData.Application;
var application = uiApplication.Application;
var uiDocument = uiApplication.ActiveUIDocument;
var document = uiDocument.Document;
// Prompt the user to select some walls
var references = uiDocument.Selection
.PickObjects(
ObjectType.Element,
new WallSelectionFilter(),
"Please select walls");
var components = references.Select(r => document.GetElement(r)).ToList();
// Start a transaction
using (Transaction t = new Transaction(document, "Change Wall Join Behavior"))
{
t.Start();
// Loop through the selected walls and change their join behavior
foreach (Reference reference in references)
{
Wall wall = document.GetElement(reference) as Wall;
WallUtils.DisallowWallJoinAtEnd(wall, 0);
WallUtils.DisallowWallJoinAtEnd(wall, 1);
}
// Commit the transaction
t.Commit();
}
return Result.Succeeded;
}
public class WallSelectionFilter : ISelectionFilter
{
public bool AllowElement(Element elem)
{
//return elem is FamilyInstance;
return elem.Name.Contains("Precast");
}
public bool AllowReference(Reference reference, XYZ position)
{
return true;
}
}
}
}
My XAML.cs looks like this:
using Autodesk.Revit.UI;
using System.Windows.Controls;
using Adams.Commands;
using System.Windows;
namespace Adams.ui
{
public partial class Customers : UserControl
{
public UIDocument uIDocument { get; }
public ExternalCommandData commandData { get; }
public Customers(UIDocument uIDocument )
{
InitializeComponent();
}
private void btnStartExcelElementsApp_Click(object sender, RoutedEventArgs e)
{
string message = string.Empty;
PrecastDisallowJoin precastDisallow = new PrecastDisallowJoin();
precastDisallow.Execute(commandData, ref message, null);
}
}
}
Any ideas of what i should be trying?
I'm new to creating add-ins and appreciate any help offered.
If I have missed any critical info please let me know.
Thank you all
When I tried the above it crashes Revit.
Im not sure how to pass the required information in the Execute method in the XAML.
The Revit dockable dialogue and hence your WPF form lives in a modeless context. It does not execute within a valid Revit API context. A valid Revit API context is only provided by Revit itself, within the event handlers called by Revit when specific events are raised. For instance, clicking a button to launch an add-in external command raises the IExternalCommand.Execute event.
The Building Coder shares a long list of articles on Idling and External Events for Modeless Access and Driving Revit from Outside
explaining how to gain access to a valid Revit API context from a modeless state.
You can address your task by using an external event:
Idling Enhancements and External Events
External Command Lister and Adding Ribbon Commands
External Event and 10 Year Forum Anniversary
Implementing the TrackChangesCloud External Event
Vipassana and Idling versus External Events
The question has also been discussed many times in the Revit API discussion forum, so you can check there for threads including WPF, dockable and external event.
You can use IExternalEventHandler:
public class MyExternalEvent : IExternalEventHandler
{
public void Execute(UIApplication app)
{
//do your revit related stuff here
}
public string GetName()
{
return "xxx";
}
}
Create external event:
ExternalEvent myExEvent= ExternalEvent.Create(new MyExternalEvent());
In order to effectively use the above you will have to hold reference to "myExEvent" in some ViewModelClass then you will be able to raise this event inside your xaml.cs:
ViewModelClass.TheEvent = myExEvent;
ViewModelClass.TheEvent.Raise();
EDIT: What you were trying to do is unfortunately not acceptable with revit API. WPF window displayed as dockpanel does not have access to valid revit api context. IExternalEventHandler gives you the possibility to somehow link dockpanel user interface with revit api.

MVVM Light for Xamarin Forms and WPF

I'm trying to make an cross-plattform app, that will work for Xamarin.Forms and WPF (like this: https://github.com/C0D3Name/XamFormsWpf) in combination with MVVM light.
MVVM Light is quite new to me and i didn't find a clear tutorial for what i want to do. On Xamarin.Forms the DependencyInjection is done by
SQLiteConnection database = DependencyService.Get<ISQLite>().GetConnection("db.sqlite");
How is this done in MVVM light? Do i have to pass the different plattform-implementations of ISQLite in as parameter?
I already created the ViewModelLocator in my PCL:
public class ViewModelLocator
{
/// <summary>
/// Register all the used ViewModels, Services et. al. witht the IoC Container
/// </summary>
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<MainViewModel>();
// My DataService is using the connection from ISQlite
SimpleIoc.Default.Register<IDataService, DataService>();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
"CA1822:MarkMembersAsStatic",
Justification = "This non-static member is needed for data binding purposes.")]
public MainViewModel Main
{
get
{
return ServiceLocator.Current.GetInstance<MainViewModel>();
}
}
}
public interface ISQLite
{
SQLiteConnection GetConnection(string sqliteFilename);
}
As far as i understood the SimpleIoc, i have to register the interface to a concrete implementation, but how does my DataService know about the correct ISQLite Implementation?
I hope my question is understandable.
From official docs:
SimpleIoc - A very simple IOC container with basic functionality needed to register and resolve instances.
DependencyService - Xamarin.Forms allows developers to define behavior in platform-specific projects. DependencyService then finds the right platform implementation, allowing shared code to access the native functionality.
So you would want to use SimpleIoc to create your PCL dependency injection graph (IDataService for example)
And you will have to use DependencyService to provide platform specific functionality. One example for DependencyService use would be loading a html file located on the device into webview. Since the locations of assets on iOS and Android are different you will have add platform specific implementation for base url and then use DependencyService in your pcl. Another example would be IO.
So in your case if you need platform specific implementations of ISQlite you will have to use DependencyService. If not you may want (or not) to use SimpleIoc to add concrete implementation of ISQlite interface to your dependency graph.
Hope that helps.
Also have a look at Akavache. We use it with a great success in Xamarin.Forms projects.
Finally i found a working solution. The (async) Sqlite-Connection is used as parameter for my ViewModelLocator
public class App : Application
{
private static ViewModelLocator locator;
public static ViewModelLocator Locator
{
get
{
if (locator == null)
{
var connection = DependencyService.Get<ISQLite>().GetConnection("db.sqlite");
locator = new ViewModelLocator(connection);
}
return locator;
}
}
}
...and the locator injects the connection into the DataService constructor:
public class ViewModelLocator
{
public ViewModelLocator(SQLiteConnectionWithLock connection)
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
SimpleIoc.Default.Register<IDataService>(() => new DataService(connection));
// ViewModels
SimpleIoc.Default.Register<MainViewModel>();
}
public MainViewModel Main => ServiceLocator.Current.GetInstance<MainViewModel>();
}

WPF Creating a Command Library

New to WPF/XAML and looking to build a Command library for my application. Current version of my application is "flat" meaning all the code behind resides in my MainWindow.cs file. I've decided to separate things out into User Controls and all the UI part so far works great but....
I'm having trouble binding my commands to things now that I've done this. To make it simple I created a new WPF project, added my Menu control (Controls\Menu.xaml) and referenced this in my MainWindow.xaml. Now, I've gone ahead and added a CommandLibrary class and can't seem to get anything working. Here is my File -> New command code:
public static class MyAppCommands
{
private static RoutedUICommand _NewItem;
static MyAppCommands()
{
_NewItem = new RoutedUICommand("Create a new project", "NewItem", typeof(MyAppCommands));
}
public static RoutedUICommand NewItem
{
get { return _NewItem; }
}
private string filePath = null;
private bool dataChanged = false;
public static void NewItem_Executed(object sender, ExecutedRoutedEventArgs e)
{
if (dataChanged)
{
string sf = SaveFirst();
if (sf != "Cancel")
{
ClearState();
}
}
else
{
ClearState();
}
}
public static void NewItem_CanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
public static void BindCommandsToWindow(Window window)
{
window.CommandBindings.Add(new CommandBinding(NewItem, NewItem_Executed, NewItem_CanExecute));
}
Where do I put the below in my class as a lot of my command will be using them...?
private string filePath = null;
private bool dataChanged = false;
Many Thanks!
I think you could benefit from setting up some design patterns like MVC or MVVM.
Check out these frameworks:
Caliburn (my favorite)
Prism
You can also follow MVC or MVVM just using a couple small helper classes, check out ariticles around the web, like this one MVVM.
Check this if you want a light-weight framework: MVVM Foundation

how to cleanup view model properly?

I have a view model that is used as the data source for my custom control. In the view model's constructor I set up a WMI ManagementEventWatcher and start it. My view model implements IDisposable, so I stop the watcher in the Dispose method.
When I embed the custom control into a window, and then close the window to exit the application it throws an InvalidComObjectException saying "COM object that has been separated from its underlying RCW cannot be used". This happens because of my watcher, and if I do not create it, there is no exception. there is no additional information about the exception such as stack trace, etc.
My guess is that something keeps the view model until the thread that the watcher uses terminates but before the watcher is stopped, and I do not know how to handle this.
Any advice?
Thanks
Konstantin
public abstract class ViewModelBase : IDisposable, ...
{
...
protected virtual void OnDispose() { }
void IDisposable.Dispose()
{
this.OnDispose();
}
}
public class DirectorySelector : ViewModelBase
{
private ManagementEventWatcher watcher;
private void OnWMIEvent(object sender, EventArrivedEventArgs e)
{
...
}
protected override void OnDispose()
{
if (this.watcher != null)
{
this.watcher.Stop();
this.watcher = null;
}
base.OnDispose();
}
public DirectorySelector()
{
try
{
this.watcher = new ManagementEventWatcher(new WqlEventQuery(...));
this.watcher.EventArrived += new EventArrivedEventHandler(this.OnWMIEvent);
this.watcher.Start();
}
catch (ManagementException)
{
this.watcher = null;
}
}
}
this article has the solution: Disposing WPF User Controls
basically, WPF dos not seem to use IDisposable anywhere, so the app needs to cleanup itself explicitly. so in my case, i subscribe to the Dispatcher.ShutdownStarted event from my control that uses the view model that needs to be disposed, and dispose the control's DataContext from the event handler.

Resources