Command And View Plugin

Description

A command and view plugin provides a connection between the client and the administration. The data transfer is completely managed, so you don't have to care about it a lot. For such a plugin are two projects necessary: One for the client and one for the administration.


Needed libraries


Let's start

This will be a plugin which provides two dll files: One for the client and one for the administration. We'll put both in one solution. The recommended way is to name your administration plugin like MyAwesomeFeature and the client plugin then MyAwesomeFeature.Payload. That makes it easy to know which library has which purpose. Let's start by creating the solution. Because the first plugin (which gives the solution the name) will be the administration plugin, we have to address the .Net Framework 4.5 and create a WPF User Control Library.



We will directly create the project for the client: To add a new project to our solution, you have to right click the root item in the Solution Explorer > Add > New Project...



Now, because our second project will be loaded by the client, it must address the .Net Framework 3.5. As I said above, we'll name this like our administration plugin but ending with a .Payload.



You can remove the entries generated by default in each project (Class1.cs and UserControl1.xaml) so our solution explorer looks like that:



We will start with adding the needed references. For the administration plugin, you should add the following libraries: Orcus.Administration.Plugins.dll, Orcus.Shared.dll and Sorzus.Wpf.Toolkit.dll

The client plugin needs the following references: Orcus.Plugins.dll and Orcus.Shared.dll. You can find the libraries in the /libraries folder of your Orcus package. Make sure that you have the newest version.


The administration plugin

We will start with creating the entry file: We create a new class called Plugin (the name doesn't matter, but it makes clear that it's the entry point) and implement the interface ICommandAndViewPlugin. Please make sure that the class is public!

using System;
using Orcus.Administration.Plugins;

namespace MyAwesomeFeature
{
    public class Plugin : ICommandAndViewPlugin
    {
        public Type Command { get; }
        public Type CommandView { get; }
        public Type View { get; }
    }
}

As you can see, there are three properties defined, all are just types. It's important to note that the Command must inherit Command, the CommandView must implement ICommandView and the View must inherit FrameworkElement.

Let's begin. We create three new items: Two classes and one UserControl.



I'll name my first class MyAwesomeFeatureCommand, my second class ViewModel and the UserControl View.



Now, we come to part which might be a little bit diffcult for developers who don't have experience with WPF/MVVM. But that's not so bad, it's makes a lot of fun if you understood it and if you don't you can also continue like you're always doing (no MVVM).

Orcus automatically connects the View to the CommandView by setting the DataContext of the View to the CommandView. So, if you access the DataContext property of your View, it will be your CommandView class.

Anyway, first, we will implement and inherit all necessary stuff. The View is fine, UserControl inherits FrameworkElement by default. The MyAwesomeFeatureCommand class must inherit Command:

using System;
using Orcus.Administration.Plugins;

namespace MyAwesomeFeature
{
    class MyAwesomeFeatureCommand : Command
    {
        public override void ResponseReceived(byte[] parameter)
        {
            throw new NotImplementedException();
        }

        protected override uint GetId()
        {
            throw new NotImplementedException();
        }
    }
}

This is the logic part of your plugin. GetId() must return a unique id. You have to generate it using TODO.

We will add a simple method which will send a few bytes to the client:

using System.Text;
using Orcus.Administration.Plugins;

namespace MyAwesomeFeature
{
    class MyAwesomeFeatureCommand : Command
    {
        public override void ResponseReceived(byte[] parameter)
        {
            //Here we'll process the response sent by the client
        }

        public void SendTextToClient(string text)
        {
            ConnectionInfo.SendCommand(this, Encoding.UTF8.GetBytes(text));
            LogService.Send("Send a few bytes to the client");
        }

        protected override uint GetId()
        {
            return 23894873;
        }
    }
}

As you can see, we have access to the ConnectionInfo which provides everything to communicate with the client. The first parameter must be our command, the second parameter are our raw bytes. The ConnectionInfo will automatically compress the bytes if necessary, the client will exactly receive these bytes.

The next thing is the ViewModel which must implement ICommandView. Also, it's recommended to inherit from PropertyChangedBase which can be found in the Sorzus.Wpf.Tookit-library. The class PropertyChangedBase implements and manages INotifyPropertyChanged, you will see:

using System.Windows;
using Orcus.Administration.Plugins;
using Sorzus.Wpf.Toolkit;

namespace MyAwesomeFeature
{
    class ViewModel : PropertyChangedBase, ICommandView
    {
        private RelayCommand _buttonClickCommand;
        private MyAwesomeFeatureCommand _myAwesomeFeatureCommand;
        private string _customText;

        public string CustomText
        {
            get { return _customText; }
            set { SetProperty(value, ref _customText); }
        }

        public RelayCommand ButtonClickCommand
        {
            get
            {
                return _buttonClickCommand ?? (_buttonClickCommand = new RelayCommand(parameter =>
                {
                    if (string.IsNullOrWhiteSpace(CustomText))
                    {
                        MessageBoxEx.Show(Application.Current.MainWindow, "The text must not be empty!");
                        return;
                    }

                    _myAwesomeFeatureCommand.SendTextToClient(CustomText);
                    CustomText = null;
                }));
            }
        }

        public void Dispose()
        {
            //Here you can dispose everything you've created
        }

        public void Initialize(IClientController clientController, ICrossViewManager crossViewManager)
        {
            _myAwesomeFeatureCommand = clientController.Commander.GetCommand();
        }

        public void LoadView()
        {
            //Here you can put code which loads something you need. This void will be called when the user clicks on the command for the first time
        }

        public string Name { get; } = "My Awesome Command";
        public Category Category { get; } = Category.Fun;
    }
}

Okay, let me explain the code above which might be a little bit difficult. The two properties Name and Category define the appearance of your command view in the action list:



Initialize is called directly after creation and allows you to take everything from the IClientController. One thing you should do is to store your Command in a variable. You can easily do that using the ICommander as you can see in the sample code.

If you are not familiar with MVVM, the RelayCommand might confuse you. But it's not that complicated: We will create a button in the view and when the user clicks on the button, the code from the RelayCommand will be executed. As you can also see, we access the property CustomText but never set it. We don't need to. In the view, we'll create a TextBox which sets it's TextBox.Text property to our CustomText property.

Let's go to our last element, the View:

<UserControl x:Class="MyAwesomeFeature.View"
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:MyAwesomeFeature"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DataContext="{d:DesignInstance local:ViewModel}"
d:DesignHeight="300"
d:DesignWidth="300"
mc:Ignorable="d">
<Grid>
<StackPanel Width="200"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Orientation="Vertical">
<TextBox Text="{Binding CustomText}" />
<Button Margin="0,5,0,0"
Command="{Binding ButtonClickCommand}"
Content="Send Text" />
</StackPanel>
</Grid>
</UserControl>

The first thing to note is the part d:DataContext="{d:DesignInstance local:ViewModel}". It just tells the auto complete feature of Visual Studio (IntelliSense) that the DataContext has the type local:ViewModel. local is a namespace (like imports in the code) and points to the default namespace of our project: xmlns:local="clr-namespace:MyAwesomeFeature". Because your project will have another name, you have to change this part.

XAML is really like to write HTML but it's much easier because IntelliSense supports you. The first element in the UserControl is a <Grid>...</Grid>. A grid allows infinity children and they can set their position by themself. We add a StackPanel with a width of 200 which is horizontally and vertically centered in the Grid. The StackPanel stacks it's childs vertically (because we've set the Orientation to Vertical). The top element is a TextBox. As you can see, the Text-Property is bound to our CustomText property. That means that they will be equal. If we set the CustomText property in our code, the TextBox will apply that and if the user inputs text into the TextBox, CustomText will apply that.

The next element is a button. We set the Margin to 0,5,0,0 which means that the space between the element above and the button is 5 units great. We bound the Command property of the button to our ButtonClickCommand which means that it will get executed when someone clicks on the button. Here you can see how the XAML code looks in the WPF designer:



Here you can see how it would look like in the administration:



You can see that the final style will be applied in the administration. The name of the theme the administration uses is mahapps.metro


The client command

The client command will be much easier than the administration part, I swear ;). As mentioned above, the client plugin must contain a class which inherits Command.

We create a new class in the MyAwesomeFeature.Payload project called MyAwesomeFeatureCommand. Please don't just name it Command or Plugin because the name of it will be included in the exception report and it's easier to resolve your plugin if your command has a strong name. Please don't forget to make the class public:

using System.Text;
using System.Windows.Forms;
using Orcus.Plugins;

namespace MyAwesomeFeature.Payload
{
    public class MyAwesomeFeatureCommand : Command
    {
        public override void ProcessCommand(byte[] parameter, IConnectionInfo connectionInfo)
        {
            var receivedText = Encoding.UTF8.GetString(parameter);
            MessageBox.Show(receivedText);
        }

        protected override uint GetId()
        {
            return 23894873;
        }
    }
}

In GetId(), we return the same id like we did in the command for the administration. Now we just process the received bytes. In the sample code above, the string gets decoded and displayed in a MessageBox. That's it.


Some tips

  • You can use the Orcus.Shared.NetSerializer.Serializer to serializer the objects you want to send. It's optimized to make the data as small as possible
  • If you want to make a little bit more complex plugin, you can use an enum to seperate the different calls. Every first byte of the byte arrays you send would be the enum so you can easily identify the package. Just take a look at the sample plugins.

Sample Projects