新增了UI节点

This commit is contained in:
fengjiayi
2025-03-14 16:04:06 +08:00
parent 8f8644f595
commit ef11edf7f1
45 changed files with 1032 additions and 41 deletions

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Serein.Library.Api
{
/// <summary>
/// 流程中的控件
/// </summary>
public interface IFlowControl
{
/// <summary>
/// 节点执行事件
/// </summary>
void OnExecuting(object data);
}
/// <summary>
/// 自定义UI显示
/// </summary>
public interface IEmbeddedContent
{
/// <summary>
/// 获取用户控件WPF
/// </summary>
object GetUserControl();
/// <summary>
/// 获取窗体控件
/// </summary>
/// <returns></returns>
IFlowControl GetFlowControl();
}
}

View File

@@ -29,6 +29,12 @@ namespace Serein.Library
/// </summary>
Exit,
/// <summary>
/// <para>UI节点(每个节点只会执行一次对应的方法)</para>
/// <para>需要返回IEmbeddedContent接口</para>
/// <para>IEmbeddedContent接口实现由你决定</para>
/// </summary>
UI,
/// <summary>
/// <para>触发器节点,必须为标记在可异步等待的方法,建议与继承了 FlowTriggerk&lt;TEnum&gt; 的实例对象搭配使用</para>
@@ -88,6 +94,11 @@ namespace Serein.Library
/// </summary>
Flipflop,
/// <summary>
/// UI节点
/// </summary>
UI,
/// <summary>
/// 表达式操作节点
/// </summary>

View File

@@ -3,6 +3,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using static Serein.Library.Utils.EmitHelper;
@@ -27,7 +28,7 @@ namespace Serein.Library
}
/*/// <summary>
/// 更新委托方法
/// </summary>

View File

@@ -379,8 +379,12 @@ namespace Serein.Library
if (md.ActingInstance is null)
{
md.ActingInstance = context.Env.IOC.Get(md.ActingInstanceType);
if (md.ActingInstance is null)
{
md.ActingInstance = context.Env.IOC.Instantiate(md.ActingInstanceType);
}
}
object[] args = await GetParametersAsync(context);
var result = await dd.InvokeAsync(md.ActingInstance, args);
return result;

View File

@@ -9,8 +9,6 @@ namespace Serein.Library.Utils
public class ArrayHelper
{
/// <summary>
/// 数组尾部扩容
/// </summary>

View File

@@ -8,6 +8,9 @@ using System.Threading.Tasks;
namespace Serein.Library.Utils
{
/// <summary>
/// 通过字典构造动态对象
/// </summary>
public class DynamicObjectHelper
{
// 类型缓存,键为类型的唯一名称(可以根据实际需求调整生成方式)

View File

@@ -16,7 +16,6 @@ namespace Serein.Library.Utils
public static class ObjectConvertHelper
{
/// <summary>
/// 父类转为子类
/// </summary>

View File

@@ -19,7 +19,6 @@ namespace Net462DllTest.Web
{
this.plcDevice = plcDevice;
this.viewManagement = viewManagement;
}
/*

View File

@@ -43,6 +43,8 @@ namespace Serein.NodeFlow.Env
this.FlowLibraryManagement = new FlowLibraryManagement(this); // 实例化类库管理
#region
NodeMVVMManagement.RegisterModel(NodeControlType.UI, typeof(SingleUINode)); // 动作节点
NodeMVVMManagement.RegisterModel(NodeControlType.Action, typeof(SingleActionNode)); // 动作节点
NodeMVVMManagement.RegisterModel(NodeControlType.Flipflop, typeof(SingleFlipflopNode)); // 触发器节点
NodeMVVMManagement.RegisterModel(NodeControlType.ExpOp, typeof(SingleExpOpNode)); // 表达式节点

View File

@@ -39,8 +39,6 @@ namespace Serein.NodeFlow.Env
await SendCommandFunc.Invoke(msgId, theme, data);
}
/// <summary>
/// 发送请求
/// </summary>

View File

@@ -33,7 +33,6 @@ namespace Serein.NodeFlow.Env
});
}
//private readonly Func<string, object?, Task> SendCommandAsync;
private readonly RemoteMsgUtil RemoteMsgUtil;
private readonly MsgControllerOfClient msgClient;
private readonly ConcurrentDictionary<string, MethodDetails> MethodDetailss = [];
@@ -46,9 +45,6 @@ namespace Serein.NodeFlow.Env
public event LoadDllHandler OnDllLoad;
public event ProjectLoadedHandler OnProjectLoaded;
/// <summary>
/// 项目准备保存
/// </summary>
public event ProjectSavingHandler? OnProjectSaving;
public event NodeConnectChangeHandler OnNodeConnectChange;
public event NodeCreateHandler OnNodeCreate;
@@ -93,14 +89,7 @@ namespace Serein.NodeFlow.Env
/// </summary>
private bool IsLoadingNode = false;
//public void SetConsoleOut()
//{
// var logTextWriter = new LogTextWriter(msg =>
// {
// OnEnvOut?.Invoke(msg);
// });
// Console.SetOut(logTextWriter);
//}
/// <summary>
/// 输出信息
@@ -454,6 +443,7 @@ namespace Serein.NodeFlow.Env
/// <returns>被设置为起始节点的Guid</returns>
public async Task<string> SetStartNodeAsync(string nodeGuid)
{
var newNodeGuid = await msgClient.SendAndWaitDataAsync<string>(EnvMsgTheme.SetStartNode, new
{
nodeGuid

View File

@@ -0,0 +1,44 @@
using Serein.Library;
using Serein.Library.Api;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Serein.NodeFlow.Model
{
public class SingleUINode : NodeModelBase
{
public IEmbeddedContent Adapter { get; private set; }
public SingleUINode(IFlowEnvironment environment) : base(environment)
{
}
public override async Task<object> ExecutingAsync(IDynamicContext context)
{
if(Adapter is null)
{
var result = await base.ExecutingAsync(context);
if (result is IEmbeddedContent adapter)
{
this.Adapter = adapter;
context.NextOrientation = ConnectionInvokeType.IsSucceed;
}
else
{
context.NextOrientation = ConnectionInvokeType.IsError;
}
}
else
{
var p = context.GetPreviousNode(this);
var data = context.GetFlowData(p.Guid);
Adapter.GetFlowControl().OnExecuting(data);
}
return Task.FromResult<object?>(null);
}
}
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Version>1.1.0</Version>
<Version>1.1.1</Version>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

View File

@@ -1,5 +1,5 @@
# 自述
基于WPFDotnet 8的流程可视化编辑器,需二次开发。
基于Dotnet 8 的流程可视化编辑器,需二次开发。
不定期在Bilibili个人空间上更新相关的视频。
https://space.bilibili.com/33526379
@@ -16,6 +16,7 @@ https://space.bilibili.com/33526379
使用 **NodeAction** 特性标记你的方法。
* 动作节点 - Action
* 触发器节点 - Flipflop
* UI节点 - UI
# 关于 IDynamicContext 说明(重要)**
* 基本说明IDynamicContext 是节点之间传递数据的接口、载体,其实例由 FlowEnvironment 运行环境自动实现,内部提供全局单例的环境接口,用以注册、获取实例(单例模式),一般情况下,你无须关注 FlowEnvironment 对外暴露的属性方法。
* 重要概念:
@@ -25,10 +26,10 @@ https://space.bilibili.com/33526379
* 简述:枚举,标识流程运行的状态(初始化,运行中,运行完成)
* 场景:类库代码中创建了运行时间较长的异步任务、或开辟了另一个线程进行循环操作时,可以在方法入参定义一个 IDynamicContext 类型入参,然后在代码中使用形成闭包,以及时判断流程是否已经结束。另外的,如果想监听项目停止运行,可以订阅 context.Env.OnFlowRunComplete 事件。
* NextOrientation - 即将进入的分支:
* 简述:流程分支枚举, Upstream上游分支、IsSucceed真分支、IsFail假分支IsError异常分支
* 场景:允许你在类库代码中操作该属性,手动控制当前节点运行完成后,下一个会执行哪一个类别的节点。
* Exit() - 结束流程
* 简述:顾名思义,能够让你在类库代码中提前结束当前流程运行
* 简述:流程分支枚举, Upstream上游分支、IsSucceed真分支、IsFail假分支IsError异常分支
* 场景:允许你在类库代码中操作该属性,手动控制当前节点运行完成后,下一个会执行哪一个类别的节点。
* Exit() - 结束流程
* 简述:顾名思义,能够让你在类库代码中提前结束当前流程运行
# 关于 DynamicNodeType 枚举的补充说明。
## 1. 不生成节点控件的枚举值:
@@ -156,13 +157,60 @@ https://space.bilibili.com/33526379
* 入参依照Action节点。
* 返回值Task`<IFlipflopContext<TResult>>`
* 描述:接收上一节点传递的上下文,同样进入异步等待,但执行完成后不会再次等待自身(只会触发一次)。
* IFlipflopContext`<TResult>`
* 关于 IFlipflopContext`<TResult>` 接口
* 基本说明IFlipflopContext是一个接口你无须关心内部实现。
* 参数描述State状态枚举描述Succeed、Cancel、Error、Cancel如果返回Cancel则不会执行后继分支如果返回其它状态则会获取对应的后继分支开始执行。
* 参数描述Type触发状态描述External外部触发Overtime超时触发当你在代码中的其他地方主动触发了触发器则该次触发类型为External当你在创建触发器后超过了指定时间创建触发器时会要求声明超时时间则会自动触发但触发类型为Overtime触发参数未你在创建触发器时指定的值
* 参数描述Value触发时传递的参数。
* 使用场景:配合 FlowTrigger`<TEnum>` 使用例如定时从PLC中获取状态当某个变量发生改变时会通知相应的触发器如果需要可以传递对应的数据。
演示:
* 使用场景:配合 FlowTrigger`<TEnum>` 使用例如定时从PLC中获取状态当某个变量发生改变时会通知相应的触发器如果需要可以传递对应的数据。
* **UI - 自定义控件**
* 入参:默认使用上一节点返回值。
* 返回值IEmbeddedContent 接口
* 描述将类库中的WPF UserControl嵌入并显示在一个节点上显示在工作台UI中。例如在视觉处理流程中需要即时的显示图片。
* 关于 IEmbeddedContent 接口
* IEmbeddedContent 需要由你实现,框架并不负责
```
[DynamicFlow("[界面显示]")]
internal class FlowControl
{
[NodeAction(NodeType.UI)]
public async Task<IEmbeddedContent> CreateImageControl(DynamicContext context)
{
WpfUserControlAdapter adapter = null;
// 其实你也可以直接创建实例
// 但如果你的实例化操作涉及到了对UI元素修改还是建议像这里一样使用异步方法
await context.Env.UIContextOperation.InvokeAsync(() =>
{
var userControl = new UserControl();
adapter = new WpfUserControlAdapter(userControl, userControl);
});
return adapter;
}
}
public class WpfUserControlAdapter : IEmbeddedContent
{
private readonly UserControl userControl;
private readonly IFlowControl flowControl;
public WpfUserControlAdapter(UserControl userControl, IFlowControl flowControl)
{
this.userControl = userControl;
this.flowControl= flowControl;
}
public IFlowControl GetFlowControl()
{
return flowControl;
}
public object GetUserControl()
{
return userControl;
}
}
```
## 演示:
![image](https://github.com/fhhyyp/serein-flow/blob/cc5f8255135b96c6bb3669bc4aa8d8167a71c262/Image/%E6%BC%94%E7%A4%BA%20-%201.png)
![image](https://github.com/fhhyyp/serein-flow/blob/cc5f8255135b96c6bb3669bc4aa8d8167a71c262/Image/%E6%BC%94%E7%A4%BA%20-%202.png)
![image](https://github.com/fhhyyp/serein-flow/blob/8f17b786f3585cabfeef60d9ab871d43b69e5461/Image/%E6%BC%94%E7%A4%BA%20-%203.png)

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="Serein.CloudWorkbench.styles.css" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet />
</head>
<body>
<Routes />
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@@ -0,0 +1,23 @@
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
</div>
<article class="content px-4">
@Body
</article>
</main>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>

View File

@@ -0,0 +1,96 @@
.page {
position: relative;
display: flex;
flex-direction: column;
}
main {
flex: 1;
}
.sidebar {
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
}
.top-row {
background-color: #f7f7f7;
border-bottom: 1px solid #d6d5d5;
justify-content: flex-end;
height: 3.5rem;
display: flex;
align-items: center;
}
.top-row ::deep a, .top-row ::deep .btn-link {
white-space: nowrap;
margin-left: 1.5rem;
text-decoration: none;
}
.top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
text-decoration: underline;
}
.top-row ::deep a:first-child {
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 640.98px) {
.top-row {
justify-content: space-between;
}
.top-row ::deep a, .top-row ::deep .btn-link {
margin-left: 0;
}
}
@media (min-width: 641px) {
.page {
flex-direction: row;
}
.sidebar {
width: 250px;
height: 100vh;
position: sticky;
top: 0;
}
.top-row {
position: sticky;
top: 0;
z-index: 1;
}
.top-row.auth ::deep a:first-child {
flex: 1;
text-align: right;
width: 0;
}
.top-row, article {
padding-left: 2rem !important;
padding-right: 1.5rem !important;
}
}
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}

View File

@@ -0,0 +1,30 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid">
<a class="navbar-brand" href="">Serein.CloudWorkbench</a>
</div>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" />
<div class="nav-scrollable" onclick="document.querySelector('.navbar-toggler').click()">
<nav class="flex-column">
<div class="nav-item px-3">
<NavLink class="nav-link" href="" Match="NavLinkMatch.All">
<span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
</nav>
</div>

View File

@@ -0,0 +1,105 @@
.navbar-toggler {
appearance: none;
cursor: pointer;
width: 3.5rem;
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
right: 1rem;
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
.navbar-toggler:checked {
background-color: rgba(255, 255, 255, 0.5);
}
.top-row {
height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
.navbar-brand {
font-size: 1.1rem;
}
.bi {
display: inline-block;
position: relative;
width: 1.25rem;
height: 1.25rem;
margin-right: 0.75rem;
top: -1px;
background-size: cover;
}
.bi-house-door-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E");
}
.bi-plus-square-fill-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E");
}
.bi-list-nested-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;
}
.nav-item:first-of-type {
padding-top: 1rem;
}
.nav-item:last-of-type {
padding-bottom: 1rem;
}
.nav-item ::deep .nav-link {
color: #d7d7d7;
background: none;
border: none;
border-radius: 4px;
height: 3rem;
display: flex;
align-items: center;
line-height: 3rem;
width: 100%;
}
.nav-item ::deep a.active {
background-color: rgba(255,255,255,0.37);
color: white;
}
.nav-item ::deep .nav-link:hover {
background-color: rgba(255,255,255,0.1);
color: white;
}
.nav-scrollable {
display: none;
}
.navbar-toggler:checked ~ .nav-scrollable {
display: block;
}
@media (min-width: 641px) {
.navbar-toggler {
display: none;
}
.nav-scrollable {
/* Never collapse the sidebar for wide screens */
display: block;
/* Allow sidebar to scroll for tall menus */
height: calc(100vh - 3.5rem);
overflow-y: auto;
}
}

View File

@@ -0,0 +1,19 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -0,0 +1,36 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">Error.</h1>
<h2 class="text-danger">An error occurred while processing your request.</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>Development Mode</h3>
<p>
Swapping to <strong>Development</strong> environment will display more detailed information about the error that occurred.
</p>
<p>
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
It can result in displaying sensitive information from exceptions to end users.
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
and restarting the app.
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@@ -0,0 +1,7 @@
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.

View File

@@ -0,0 +1,64 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}

View File

@@ -0,0 +1,6 @@
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>

View File

@@ -0,0 +1,10 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Serein.CloudWorkbench
@using Serein.CloudWorkbench.Components

View File

@@ -0,0 +1,39 @@
using Serein.CloudWorkbench.Components;
using Serein.CloudWorkbench.Services;
namespace Serein.CloudWorkbench
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// ×¢²á CounterService ×÷Ϊµ¥Àý
builder.Services.AddSingleton<CounterService>();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
}
}
}

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<DeleteExistingFiles>false</DeleteExistingFiles>
<ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>bin\Release\net8.0\publish\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:30763",
"sslPort": 44329
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5085",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7230;http://localhost:5085",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,21 @@
namespace Serein.CloudWorkbench.Services
{
public class CounterService
{
public int Count { get; private set; } = 0;
public event Action? OnCountChanged;
public void Increment()
{
Count++;
OnCountChanged?.Invoke();
}
public void Decrement()
{
Count--;
OnCountChanged?.Invoke();
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,51 @@
html, body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}
a, .btn-link {
color: #006bb7;
}
.btn-primary {
color: #fff;
background-color: #1b6ec2;
border-color: #1861ac;
}
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
}
.content {
padding-top: 1.1rem;
}
h1:focus {
outline: none;
}
.valid.modified:not([type=checkbox]) {
outline: 1px solid #26b050;
}
.invalid {
outline: 1px solid #e50000;
}
.validation-message {
color: #e50000;
}
.blazor-error-boundary {
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
padding: 1rem 1rem 1rem 3.7rem;
color: white;
}
.blazor-error-boundary::after {
content: "An error has occurred."
}
.darker-border-checkbox.form-check-input {
border-color: #929292;
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -17,13 +17,13 @@ namespace Serein.Workbench
{
#if DEBUG
if (1 == 2)
if (1 == 1)
{
// 这里是测试代码,可以删除
string filePath;
filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\Release\net8.0\PLCproject.dnf";
filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\Release\banyunqi\project.dnf";
filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\debug\net8.0\project.dnf";
filePath = @"F:\临时\project\project.dnf";
//filePath = @"C:\Users\Az\source\repos\CLBanyunqiState\CLBanyunqiState\bin\debug\net8.0\test.dnf";
string content = System.IO.File.ReadAllText(filePath); // 读取整个文件内容
App.FlowProjectData = JsonConvert.DeserializeObject<SereinProjectData>(content);

View File

@@ -162,6 +162,7 @@ namespace Serein.Workbench
IOCObjectViewer.SelectObj += ViewObjectViewer.LoadObjectInformation; // 使选择 IOC容器视图 的某项(对象)时,可以在 数据视图 呈现数据
#region NodeControlType Control类型 ViewModel类型
NodeMVVMManagement.RegisterUI(NodeControlType.UI, typeof(UINodeControl), typeof(UINodeControlViewModel));
NodeMVVMManagement.RegisterUI(NodeControlType.Action, typeof(ActionNodeControl), typeof(ActionNodeControlViewModel));
NodeMVVMManagement.RegisterUI(NodeControlType.Flipflop, typeof(FlipflopNodeControl), typeof(FlipflopNodeControlViewModel));
NodeMVVMManagement.RegisterUI(NodeControlType.ExpOp, typeof(ExpOpNodeControl), typeof(ExpOpNodeControlViewModel));
@@ -492,6 +493,9 @@ namespace Serein.Workbench
case Library.NodeType.Flipflop:
dllControl.AddFlipflop(methodDetailsInfo); // 添加触发器方法到控件
break;
case Library.NodeType.UI:
dllControl.AddUI(methodDetailsInfo); // 添加触发器方法到控件
break;
}
}

View File

@@ -15,6 +15,7 @@
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
@@ -26,6 +27,9 @@
<GroupBox x:Name="FlipflopNodeGroupBox" Grid.Row="1" Header="触发器" Margin="5">
<ListBox x:Name="FlipflopsListBox" Background="#FACFC1"/>
</GroupBox>
<GroupBox x:Name="UINodeGroupBox" Grid.Row="2" Header="UI" Margin="5">
<ListBox x:Name="UIListBox" Background="#FFFBD7"/>
</GroupBox>
</Grid>

View File

@@ -6,10 +6,7 @@ using System.Windows.Input;
namespace Serein.Workbench.Node.View
{
/// <summary>
/// UserControl1.xaml 的交互逻辑
@@ -28,8 +25,11 @@ namespace Serein.Workbench.Node.View
this.nodeLibraryInfo = nodeLibraryInfo;
Header = "DLL name : " + nodeLibraryInfo.AssemblyName;
InitializeComponent();
}
FlipflopNodeGroupBox.Visibility = Visibility.Collapsed;
ActionNodeGroupBox.Visibility = Visibility.Collapsed;
UINodeGroupBox.Visibility = Visibility.Collapsed;
}
/// <summary>
@@ -65,6 +65,16 @@ namespace Serein.Workbench.Node.View
FlipflopNodeGroupBox.Visibility = Visibility.Visible;
}
/// <summary>
/// 向触发器面板添加类型的文本块
/// </summary>
/// <param name="type">要添加的类型</param>
public void AddUI(MethodDetailsInfo mdInfo)
{
AddTypeToListBox(mdInfo, UIListBox);
UINodeGroupBox.Visibility = Visibility.Visible;
}
/// <summary>
/// 向指定面板添加类型的文本块
/// </summary>
@@ -137,6 +147,7 @@ namespace Serein.Workbench.Node.View
{
NodeType.Action => NodeControlType.Action,
NodeType.Flipflop => NodeControlType.Flipflop,
NodeType.UI => NodeControlType.UI,
_ => NodeControlType.None,
},
MethodDetailsInfo = mdInfo,

View File

@@ -0,0 +1,40 @@
<local:NodeControlBase x:Class="Serein.Workbench.Node.View.UINodeControl"
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"
xmlns:local="clr-namespace:Serein.Workbench.Node.View"
xmlns:vm="clr-namespace:Serein.Workbench.Node.ViewModel"
xmlns:themes="clr-namespace:Serein.Workbench.Themes"
d:DataContext="{d:DesignInstance vm:UINodeControlViewModel}"
mc:Ignorable="d"
MinWidth="50"
Initialized="NodeControlBase_Initialized"
Loaded="NodeControlBase_Loaded"
>
<Grid x:Name="MainGrid">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Background="#E7EFF5" >
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<local:ExecuteJunctionControl Grid.Column="0" MyNode="{Binding NodeModel}" x:Name="ExecuteJunctionControl" HorizontalAlignment="Left" Grid.RowSpan="2"/>
<Border Grid.Column="1" BorderThickness="1" HorizontalAlignment="Stretch">
<TextBlock Text="UI控件" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</Grid>
<Border Grid.Row="1" x:Name="EmbedContainer" BorderBrush="Black" BorderThickness="1"
Width="500" Height="400"/>
</Grid>
</local:NodeControlBase>

View File

@@ -0,0 +1,65 @@
using Serein.Workbench.Node.ViewModel;
using Serein.Workbench.Tool;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace Serein.Workbench.Node.View
{
/// <summary>
/// UINodeControl.xaml 的交互逻辑
/// </summary>
public partial class UINodeControl : NodeControlBase, INodeJunction
{
public UINodeControl()
{
InitializeComponent();
}
public UINodeControl(UINodeControlViewModel viewModel) : base(viewModel)
{
DataContext = viewModel;
InitializeComponent();
}
public JunctionControlBase ExecuteJunction => this.ExecuteJunctionControl;
public JunctionControlBase NextStepJunction => throw new NotImplementedException();
public JunctionControlBase[] ArgDataJunction => throw new NotImplementedException();
public JunctionControlBase ReturnDataJunction => throw new NotImplementedException();
private void NodeControlBase_Loaded(object sender, RoutedEventArgs e)
{
UINodeControlViewModel vm = (UINodeControlViewModel)DataContext;
vm.InitAdapter(userControl => {
EmbedContainer.Child = userControl;
});
}
private void NodeControlBase_Initialized(object sender, EventArgs e)
{
UINodeControlViewModel vm = (UINodeControlViewModel)DataContext;
}
}
}

View File

@@ -0,0 +1,40 @@
using Serein.Library;
using Serein.Library.Api;
using Serein.NodeFlow.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace Serein.Workbench.Node.ViewModel
{
public class UINodeControlViewModel : NodeControlViewModelBase
{
private SingleUINode NodeModel => (SingleUINode)base.NodeModel;
//public IEmbeddedContent Adapter => NodeModel.Adapter;
public UINodeControlViewModel(NodeModelBase nodeModel) : base(nodeModel)
{
//NodeModel.Adapter.GetWindowHandle();
}
public void InitAdapter(Action<UserControl> setUIDisplayHandle)
{
Task.Factory.StartNew(async () =>
{
var context = new DynamicContext(NodeModel.Env);
await NodeModel.ExecutingAsync(context);
if (context.NextOrientation == ConnectionInvokeType.IsSucceed
&& NodeModel.Adapter.GetUserControl() is UserControl userControl)
{
NodeModel.Env.UIContextOperation.Invoke(() =>
{
setUIDisplayHandle.Invoke(userControl);
});
}
});
}
}
}

View File

@@ -51,14 +51,18 @@
<ProjectReference Include="..\NodeFlow\Serein.NodeFlow.csproj" />
<ProjectReference Include="..\Serein.Script\Serein.Script.csproj" />
</ItemGroup>
<ItemGroup>
<Reference Include="WindowsFormsIntegration" />
<Reference Include="System.Windows.Forms" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Lagrange.Core" Version="0.3.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="ZXing.Net" Version="0.16.10" />
<PackageReference Include="ZXing.Net.Bindings.ImageSharp" Version="0.16.15" />
<!--<PackageReference Include="Lagrange.Core" Version="0.3.1" />-->
<!--<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />-->
<!--<PackageReference Include="ZXing.Net.Bindings.ImageSharp" Version="0.16.15" />-->
<!--<PackageReference Include="MySqlConnector" Version="2.4.0" />
<PackageReference Include="SqlSugarCore" Version="5.1.4.170" />
@@ -74,4 +78,9 @@
</Compile>
</ItemGroup>
<ItemGroup>
<Folder Include="Views\" />
<Folder Include="VIewModels\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,64 @@
using Serein.Library.Api;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Interop;
namespace Serein.Workbench.Tool
{
/*public class EmbeddedHost : HwndHost
{
private readonly IntPtr _hwnd;
public EmbeddedHost(IEmbeddedContent content)
{
_hwnd = content.GetWindowHandle();
}
protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{
if (_hwnd == IntPtr.Zero)
throw new InvalidOperationException("无效的窗口句柄");
// 设置窗口为子窗口(必须去掉 WS_POPUP添加 WS_CHILD
SetWindowLongPtr(_hwnd, GWL_STYLE, GetWindowLongPtr(_hwnd, GWL_STYLE) | WS_CHILD);
SetParent(_hwnd, hwndParent.Handle);
// 让窗口填充整个区域
SetWindowPos(_hwnd, IntPtr.Zero, 0, 0, (int)ActualWidth, (int)ActualHeight,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW);
return new HandleRef(this, _hwnd);
}
protected override void DestroyWindowCore(HandleRef hwnd)
{
// 窗口销毁时的操作(如果需要)
}
// WinAPI 导入
private const int GWL_STYLE = -16;
private const int WS_CHILD = 0x40000000;
[DllImport("user32.dll")]
private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
[DllImport("user32.dll", SetLastError = true)]
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowLongPtr(IntPtr hWnd, int nIndex);
[DllImport("user32.dll", SetLastError = true)]
private static extern int SetWindowLongPtr(IntPtr hWnd, int nIndex, int dwNewLong);
private const uint SWP_NOZORDER = 0x0004;
private const uint SWP_NOACTIVATE = 0x0010;
private const uint SWP_SHOWWINDOW = 0x0040;
}*/
}