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
WPF framework: mahapps Metro reference
MVVM:Caliburn. Micro reference
Log: NLog reference
Communication: dotnetty Transport. Libuv reference
ICON:MahApps. Metro. IconPacks. Fontawesome reference
Elastic and transient fault handling Library: Polly reference
Documentation tool: sandcastle reference
Packing tool: Wix reference
Fluent httpclient reference
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 resxresourcemanager in vs
As shown in the figure:
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:
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. Micro.
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:
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();