Guide

Main program directory structure

├─ Core
│  ├─ WHS.Infrastructure
│  └─ WPFLocalizeExtension
├─ Document
│  └─WHSDocumentation
├─ Plugins
│  ├─ WHS.App.Animation
│  ├─ WHS.DEVICE.AUDIO
│  ├─ WHS.DEVICE.MAPDESIGN
│  ├─ WHS.DEVICE.ROBOT3D
│  ├─ WHS.DEVICE.ROBOTNEW
│  ├─ WHS.DEVICE.SIGNATURE
│  └─ WHS.DEVICE.WEIGHT
├─ Setup
│  ├─ WHS_CustomAction
│  └─ WHSSetup
├─ Templates
│  └─ WHSPlugin5
│  └─  └─ WHSPlugin5
└─ WHS

Environment

Program environment: net5. 0-windows

Contribution

  1. WPF framework: mahapps Metro referenceopen in new window

  2. MVVM:Caliburn. Micro referenceopen in new window

  3. Log: NLog referenceopen in new window

  4. Communication: dotnetty Transport. Libuv referenceopen in new window

  5. ICON:MahApps. Metro. IconPacks. Fontawesome referenceopen in new window

  6. Elastic and transient fault handling Library: Polly referenceopen in new window

  7. Documentation tool: sandcastle referenceopen in new window

  8. Packing tool: Wix referenceopen in new window

  9. Fluent httpclient referenceopen in new window

Plug in directory structure


├─ Properties
│  └─ Resource.resx //images are added to the resource
├─ Actions
│  └─ActionDemo.cs  //command from websocket
├─ Images
│  └─plugin.png
├─ Model
│  └─ActionModel.cs 
├─ Resources
│  ├─ Strings.en.resx  //language en
│  ├─ Strings.resx     //the default language is Chinese
│  └─ Strings.zh-CN.resx  //language Chinese
├─ ViewModels     //MVVM:DeviceView
│  └─ DeviceViewModel.cs
├─ Views         
│  ├─ DeviceView.cs
│  └─ DeviceView.xaml
├─ DevicePluginDefinition.cs //plug in configuration definition
└─ plugin.def       //plug-in definition

Multilingual

Main program multilingual

Add a language resource in the Resources folder of the WHS assembly, and the system will automatically identify the culture to which the language belongs

It is recommended to install a plug-in resxresourcemanageropen in new window in vs

As shown in the figure:

main program multilingual

Plug in multilingual

When a language is added to the main program, the plug-in also needs to add corresponding language resources.

At the same time, after compilation, the command will copy the language folder generated by the plug-in to the plugins folder of the main program

As shown in the figure:

plug in multilingual


mkdir $(SolutionDir)$(OutDir)Plugins\xx-pluginname-xx\xx-languageName-xx

xcopy /y /s /e "$(TargetDir)xx-languageName-xx" "$(SolutionDir)$(OutDir)Plugins\xx-pluginname-xx\xx-languageName-xx\"

TIP

XX pluginame XX plug-in directory

XX languagename XX language name, such as zh CN, EN

View

View inherits page because the main form uses the Framecomponent to navigate the plug-in


    /// <summary>
    /// DeviceView.xaml 的交互逻辑
    /// </summary>
    public partial class DeviceView : Page
    {
        public DeviceView()
        {
            InitializeComponent();
        }
    }

Traditional development

Students who traditionally use WinForm or WPF can develop in the traditional way

MVVM mode development

Due to the use of Caliburn. Microopen in new window.

An attempt will automatically find the corresponding ViewModel by name

xxView>>>>>>xxViewModel

├─ ViewModels
│  └─ DeviceViewModel.cs
├─ Views
│  ├─ DeviceView.cs
└─ └─ DeviceView.xaml

Command

Why use commands in programs?

The main program introduces dotnetty and opens port 18080 as websocket and HTTP by default. When the plug-ins are developed, especially when the plug-ins we develop are convenient for hardware, We hope that users can use unified rules to access our plug-ins through the web or other programs.

Command transfer model request

Send: (Global request format)


{
"Params": {},
"ID": "xxxxxxx",
"Action": "command"
}

TIP

Params: pass in different object types according to different commands ID: string type

A unique credential for the client to initiate a command (there are multiple commands initiated at the same time).

When returning, because the hardware service returns asynchronously, the corresponding ID is returned according to the initiated command,

In this way, the client knows which command is returned.

Action: string type

Command transfer model response

Global return format: (response, callback)

{
"errCode": 0,
"errText": "",
"params": {
"Source": “”,
"Result": {}
},
"ID": "xxxxxxx",
"Action": "command"
}

TIP

ErrCode: when it is 0, it means there is an error. You can view errtext

Params: if there is no return value, this node may be null

Source: source device.

Result: returns different object types according to different commands.

ID: the ID from the request

Action: RESPONSE will be returned according to request.

Callback will be different, which is equivalent to the server actively sending messages to the client.

How to execute the command?

All commands are returned in asynchronous mode

Self test tool

Users can click about - > WHS web test in the program

As shown in the figure:

plug in multilingual

Thermal loading

Versions above vs2019 already support hot reload

Why does the plug-in need a hot load?

When the program reflects the sub plug-in, the general loading method is:

When the main program is running, if the plug-in is updated, you need to close the main program to apply the new plug-in

The concept of hot loading is:

When the main program is running, if the plug-in is updated, the new plug-in content can be loaded without closing the main program


<plugin>
<file name="xxxxxxxx.dll"/>
<!-- anycpu x64 x86 arm arm64 wasm -->
<runPlatform target="anycpu" />
<enableHotReload>true</enableHotReload>
</plugin>

TIP

Enablehotreload is set to true for hot load

WARNING

When the plug-in development uses Pinvoke Net to load C or C + + files.

Enablehotreload needs to be set to false

Advanced

Plug in interception

For example: when the plug-in is loaded, you need to access the server through HTTP to obtain the token

public class AuthPulginInterceptor : IPluginInterceptor
    {

        public void AfterHandle()
        {

        }

        public bool PreHandle(MessageRequest requestmessage)
        {
            if (PluginContext.AuthModel == null)
            {
                var view = GlobalContext.SimpleContainer.GetInstance<ViewModels.PrintViewModel>();

                try
                {
                    var client = new FluentClient(ServerSettings.ApiUrl);
                    var response = client.GetAsync(PluginContext.AuthAddress)
                       .WithArgument("ClientId", "WHS#" + HardwareID.Value())
                       .WithArgument("ClientSecret", HardwareID.Value())
                       .WithArgument("GrantType", "client_credential")

                       .WithOptions(true, true)
                       .AsResponse().Result;
                    client.Dispose();

                    if (response.Status == System.Net.HttpStatusCode.OK)
                    {
                        PluginContext.AuthModel = response.As<AuthModel>().Result;
                        view.PrintStatusBrush = Brushes.Blue;
                        view.PrintStatus = "xxxxxX";
                        view.BtnVisibility = Visibility.Hidden;
                        return true;
                    }
                    else
                    {
                        view.PrintStatusBrush = Brushes.Red;
                        view.PrintStatus = "XXXXXX";
                        view.BtnVisibility = Visibility.Visible;
                        ErrorMessageModel messageModel = response.As<ErrorMessageModel>().Result;
                        MessageResponse res = new MessageResponse();
                        res.ID = requestmessage.ID;
                        res.ChannelID = requestmessage.ChannelID;
                        res.Action = requestmessage.Action;
                        res.errCode = messageModel.Code;
                        res.errText = messageModel.Message + "。XXXXXXXXX";
                        EnvironmentManager.Instance.PostResponseMessage(res);
                        return false;
                    }
                }
                catch (Exception ex)
                {
                    view.PrintStatusBrush = Brushes.Red;
                    view.PrintStatus = "XXXXXXX(error)";
                    view.BtnVisibility = Visibility.Visible;
                    MessageResponse res = new MessageResponse();
                    res.ID = requestmessage.ID;
                    res.ChannelID = requestmessage.ChannelID;
                    res.Action = requestmessage.Action;
                    res.errCode = 400;
                    res.errText = ex.Message;
                    EnvironmentManager.Instance.PostResponseMessage(res);
                    return false;
                }
            }
            return true;
        }
    }

Register interception in PluginConfigurationDefinition

public override void Init()
{
    ///xxxx
    base. RegistPulginInterceptor(new AuthPulginInterceptor());
    ///xxxx
}

HTTP request interception

For example: when the server of HTTP access returns unauthorized. Try again, use refresh token to get a new token again, and resubmit the failed data

  public class RetryTokenCoordinator : IRequestCoordinator
    {
        public Task<HttpResponseMessage> ExecuteAsync(IRequest request, Func<IRequest, Task<HttpResponseMessage>> dispatcher)
        {
            return Policy
               .HandleResult<HttpResponseMessage>(response =>
               {
                   return response.StatusCode == HttpStatusCode.Unauthorized;
               })
               .RetryAsync(1, async (response, retryCount, context) =>
               {
                   //Refresh the logic of token
                   var client = new FluentClient(ServerSettings.ApiUrl);
                   var refreshResponse = await client.GetAsync(PluginContext.AuthAddress)
                    .WithArgument("ClientId", "WHS" + HardwareID.Value())
                    .WithArgument("ClientSecret", HardwareID.Value())
                    .WithArgument("GrantType", "refresh_token")
                    .WithArgument("RefreshToken", PluginContext.AuthModel.RefreshToken)
                    .WithOptions(true, true)
                    .AsResponse();
                   client.Dispose();
                   if (refreshResponse.Status == HttpStatusCode.OK)
                   {
                       //Get the new token and refresh after the refresh is successful_ Token and other information
                       PluginContext.AuthModel = await refreshResponse.As<AuthModel>();
                      //Replace the last failed access token
                       await request.WithBearerAuthentication(PluginContext.AuthModel.Token);
                   }
               })
               .ExecuteAsync(() =>
               {
                  //Execute last request
                   return dispatcher(request);
               });
        }
    }

Usage:

 var client = new FluentClient(ServerSettings.ApiUrl);
                    var templateResponse = client.GetAsync(PluginContext.TemplateAddress)
                       .WithArgument("printUid", printTask.PrintUID.ToString())
                       .WithBearerAuthentication(PluginContext.AuthModel.Token)
                       .WithHeader("CfgId", printTask.CfgId.ToString())
                       .WithRequestCoordinator(new RetryTokenCoordinator())
                       .WithOptions(true, true)
                       .AsResponse().Result;
                    client.Dispose();