子程序使用优化
This commit is contained in:
@@ -44,6 +44,16 @@ namespace ACP.ViewModels
|
||||
private readonly DispatcherTimer _uiRefreshTimer;
|
||||
#endregion
|
||||
|
||||
#region 子程序导航
|
||||
|
||||
/// <summary>
|
||||
/// 当前台架是否可以返回上一级
|
||||
/// </summary>
|
||||
public bool CanGoBack => CurrentContext?.CanGoBack ?? false;
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性
|
||||
public string Title
|
||||
{
|
||||
@@ -237,6 +247,7 @@ namespace ACP.ViewModels
|
||||
RaisePropertyChanged(nameof(RunIcon));
|
||||
RaisePropertyChanged(nameof(RunningTime));
|
||||
RaisePropertyChanged(nameof(IsTerminate));
|
||||
RaisePropertyChanged(nameof(CanGoBack));
|
||||
}
|
||||
|
||||
#region 命令处理与事件
|
||||
@@ -519,6 +530,9 @@ namespace ACP.ViewModels
|
||||
targetContext.IsTerminate = false;
|
||||
}
|
||||
|
||||
// 清空子程序导航栈,回到根程序
|
||||
targetContext.ResetNavigation();
|
||||
|
||||
if (_globalInfo.CurrentScope == runningScope) RefreshAllContextProperties();
|
||||
}
|
||||
|
||||
@@ -601,6 +615,7 @@ namespace ACP.ViewModels
|
||||
targetContext.Program.Parameters.Clear();
|
||||
targetContext.Program.StepCollection.Clear();
|
||||
targetContext.Program.ErrorStepCollection.Clear();
|
||||
targetContext.ResetNavigation();
|
||||
foreach (var item in CurrentConfig.ParameterList)
|
||||
{
|
||||
var param = CurrentConfig.SharedParameterList.FirstOrDefault(x => x.ParameterName == item.Name);
|
||||
@@ -665,7 +680,7 @@ namespace ACP.ViewModels
|
||||
targetContext.Program.StepCollection = program.StepCollection;
|
||||
targetContext.Program.ErrorStepCollection = program.ErrorStepCollection;
|
||||
targetContext.CurrentFilePath = filePath;
|
||||
|
||||
targetContext.ResetNavigation();
|
||||
foreach (var item in CurrentConfig.SharedParameterList)
|
||||
{
|
||||
var parameter = targetContext?.Program?.Parameters?.FirstOrDefault(x => x.Name == item.ParameterName);
|
||||
|
||||
@@ -244,6 +244,8 @@ Command="{Binding GetFileStringCommand}">
|
||||
Foreground="Black" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator/>
|
||||
|
||||
</MenuItem>
|
||||
<MenuItem Header="切换初始主界面"
|
||||
FontSize="13"
|
||||
|
||||
@@ -16,6 +16,7 @@ using System.Xml;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.ViewModelBase;
|
||||
using Prism.Events;
|
||||
using Prism.Commands;
|
||||
|
||||
namespace TestingModule.ViewModels
|
||||
{
|
||||
@@ -94,6 +95,44 @@ namespace TestingModule.ViewModels
|
||||
public ICommand DeleteStepCommand { get;set; }
|
||||
public ICommand TabSelectionChangedCommand { get;set; }
|
||||
public ICommand SelectionChangedCommand { get;set; }
|
||||
public ICommand OpenSubProgramCommand { get; set; }
|
||||
public ICommand GoBackCommand { get; set; }
|
||||
#endregion
|
||||
|
||||
#region 子程序导航
|
||||
|
||||
/// <summary>
|
||||
/// 当前显示的程序(绑定到 DataGrid 的 ItemsSource 根)。
|
||||
/// 委托到 ScopedContext.CurrentProgram,保证 ShellViewModel 也能通过 ScopedContext 访问。
|
||||
/// </summary>
|
||||
public ProgramVM CurrentProgram
|
||||
{
|
||||
get => _ScopedContext.CurrentProgram;
|
||||
set
|
||||
{
|
||||
if (_ScopedContext.CurrentProgram != value)
|
||||
{
|
||||
_ScopedContext.CurrentProgram = value;
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string BreadcrumbPath
|
||||
{
|
||||
get => _ScopedContext.BreadcrumbPath;
|
||||
set
|
||||
{
|
||||
if (_ScopedContext.BreadcrumbPath != value)
|
||||
{
|
||||
_ScopedContext.BreadcrumbPath = value;
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanGoBack => _ScopedContext.CanGoBack;
|
||||
|
||||
#endregion
|
||||
|
||||
public StepsManagerViewModel(IContainerProvider containerProvider, ScopedContext scopedContext, SystemConfig systemConfig, GlobalInfo globalInfo) : base(containerProvider)
|
||||
@@ -108,9 +147,31 @@ namespace TestingModule.ViewModels
|
||||
DeleteStepCommand = new DelegateCommand(DeleteStep);
|
||||
TabSelectionChangedCommand = new DelegateCommand<string>(TabSelectionChanged);
|
||||
SelectionChangedCommand = new DelegateCommand<object>(SelectionChanged);
|
||||
OpenSubProgramCommand = new DelegateCommand(OpenSubProgram);
|
||||
GoBackCommand = new DelegateCommand(GoBack);
|
||||
SubscribeStepCollections();
|
||||
Program.PropertyChanged += Program_PropertyChanged;
|
||||
Admin = _globalInfo.IsAdmin;
|
||||
|
||||
// 初始化 CurrentProgram 为根程序
|
||||
_ScopedContext.CurrentProgram = Program;
|
||||
_ScopedContext.BreadcrumbPath = "主程序";
|
||||
|
||||
// 订阅 ScopedContext 属性变化,外部(ShellViewModel)修改时转发通知给 UI
|
||||
_ScopedContext.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ScopedContext.CurrentProgram))
|
||||
RaisePropertyChanged(nameof(CurrentProgram));
|
||||
else if (e.PropertyName == nameof(ScopedContext.BreadcrumbPath))
|
||||
RaisePropertyChanged(nameof(BreadcrumbPath));
|
||||
else if (e.PropertyName == nameof(ScopedContext.CanGoBack))
|
||||
RaisePropertyChanged(nameof(CanGoBack));
|
||||
};
|
||||
|
||||
// 订阅子程序导航事件(运行时由 StepRunning 发布)
|
||||
_eventAggregator.GetEvent<SubProgramNavigateEvent>()
|
||||
.Subscribe(OnSubProgramNavigate, ThreadOption.UIThread, false,
|
||||
payload => payload.Scope == _systemConfig.Title);
|
||||
}
|
||||
|
||||
private void SelectionChanged(object parameter)
|
||||
@@ -208,6 +269,72 @@ namespace TestingModule.ViewModels
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 子程序导航方法
|
||||
|
||||
/// <summary>
|
||||
/// 编辑模式:右键打开子程序
|
||||
/// </summary>
|
||||
private void OpenSubProgram()
|
||||
{
|
||||
if (SelectedStep == null || SelectedStep.StepType != "子程序" || SelectedStep.SubProgram == null)
|
||||
return;
|
||||
|
||||
// 压栈:保存当前程序
|
||||
_ScopedContext.SubProgramNavigationStack.Push(CurrentProgram);
|
||||
_ScopedContext.UpdateCanGoBack();
|
||||
// 更新面包屑
|
||||
var stepName = SelectedStep.Name ?? "子程序";
|
||||
BreadcrumbPath = $"{BreadcrumbPath} > {stepName}";
|
||||
// 切换到子程序
|
||||
CurrentProgram = SelectedStep.SubProgram;
|
||||
RaisePropertyChanged(nameof(CanGoBack));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回上一级程序
|
||||
/// </summary>
|
||||
private void GoBack()
|
||||
{
|
||||
if (_ScopedContext.SubProgramNavigationStack.Count == 0)
|
||||
return;
|
||||
|
||||
// 出栈:恢复上一级程序
|
||||
var parentProgram = _ScopedContext.SubProgramNavigationStack.Pop();
|
||||
_ScopedContext.UpdateCanGoBack();
|
||||
CurrentProgram = parentProgram;
|
||||
|
||||
// 更新面包屑:去掉最后一段
|
||||
var parts = BreadcrumbPath.Split(" > ");
|
||||
if (parts.Length > 1)
|
||||
BreadcrumbPath = string.Join(" > ", parts.Take(parts.Length - 1));
|
||||
else
|
||||
BreadcrumbPath = "主程序";
|
||||
|
||||
RaisePropertyChanged(nameof(CanGoBack));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 运行时:处理 StepRunning 发布的子程序导航事件
|
||||
/// </summary>
|
||||
private void OnSubProgramNavigate(SubProgramNavigatePayload payload)
|
||||
{
|
||||
if (payload.Action == NavigateAction.Enter && payload.SubProgram != null)
|
||||
{
|
||||
_ScopedContext.SubProgramNavigationStack.Push(CurrentProgram);
|
||||
_ScopedContext.UpdateCanGoBack();
|
||||
CurrentProgram = payload.SubProgram;
|
||||
var stepName = payload.StepName ?? "子程序";
|
||||
BreadcrumbPath = $"{BreadcrumbPath} > {stepName}";
|
||||
RaisePropertyChanged(nameof(CanGoBack));
|
||||
}
|
||||
else if (payload.Action == NavigateAction.Exit)
|
||||
{
|
||||
GoBack();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 辅助方法
|
||||
|
||||
/// <summary>
|
||||
@@ -304,9 +431,9 @@ namespace TestingModule.ViewModels
|
||||
Program.PropertyChanged -= Program_PropertyChanged;
|
||||
}
|
||||
|
||||
// 3. 【核心修复】必须显式退订 Prism 全局事件(AlarmEvent)
|
||||
// 注意:因为订阅时使用的是匿名 Lambda,最安全稳妥的退订方式是把整个事件上的当前 VM 订阅者全部注销
|
||||
// 3. 【核心修复】必须显式退订 Prism 全局事件
|
||||
_eventAggregator?.GetEvent<AlarmEvent>()?.Unsubscribe(null);
|
||||
_eventAggregator?.GetEvent<SubProgramNavigateEvent>()?.Unsubscribe(null);
|
||||
|
||||
// 4. 清空临时缓存集合与 UI 绑定列表,避免悬挂指针
|
||||
tmpCopyList?.Clear();
|
||||
|
||||
@@ -8,11 +8,26 @@
|
||||
xmlns:behaviors="clr-namespace:UIShare.Behaviors;assembly=UIShare"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<GroupBox Header="{Binding Title}">
|
||||
<DockPanel>
|
||||
<!-- 面包屑导航栏 -->
|
||||
<Border DockPanel.Dock="Top" Background="#F0F0F0" Padding="6,3" Visibility="{Binding CanGoBack, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Content="↩ 返回上一级"
|
||||
Command="{Binding GoBackCommand}"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
FontSize="12" Padding="8,2" Margin="8,0,0,0"/>
|
||||
<TextBlock Text="{Binding BreadcrumbPath}" VerticalAlignment="Center" FontSize="12" Foreground="#555"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<TabControl SelectedIndex="{Binding SelectedTabIndex, Mode=TwoWay}">
|
||||
<i:Interaction.Behaviors>
|
||||
<behaviors:TabControlSelectionChangedBehavior Command="{Binding TabSelectionChangedCommand}"
|
||||
@@ -29,7 +44,7 @@
|
||||
Background="Transparent"
|
||||
CanUserAddRows="False"
|
||||
CanUserSortColumns="False"
|
||||
ItemsSource="{Binding Program.StepCollection}"
|
||||
ItemsSource="{Binding CurrentProgram.StepCollection}"
|
||||
SelectedItem="{Binding SelectedStep, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectionMode="Extended"
|
||||
SelectionUnit="FullRow">
|
||||
@@ -120,6 +135,9 @@
|
||||
<MenuItem Header="删除"
|
||||
Foreground="Red"
|
||||
Command="{Binding DeleteStepCommand}" />
|
||||
<Separator/>
|
||||
<MenuItem Header="打开子程序"
|
||||
Command="{Binding OpenSubProgramCommand}" />
|
||||
</ContextMenu>
|
||||
</DataGrid.ContextMenu>
|
||||
|
||||
@@ -135,7 +153,7 @@
|
||||
Background="Transparent"
|
||||
CanUserAddRows="False"
|
||||
CanUserSortColumns="False"
|
||||
ItemsSource="{Binding Program.ErrorStepCollection}"
|
||||
ItemsSource="{Binding CurrentProgram.ErrorStepCollection}"
|
||||
SelectedItem="{Binding SelectedStep, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectionMode="Extended"
|
||||
SelectionUnit="FullRow">
|
||||
@@ -226,21 +244,16 @@
|
||||
<MenuItem Header="删除"
|
||||
Foreground="Red"
|
||||
Command="{Binding DeleteStepCommand}" />
|
||||
<Separator/>
|
||||
<MenuItem Header="打开子程序"
|
||||
Command="{Binding OpenSubProgramCommand}" />
|
||||
</ContextMenu>
|
||||
</DataGrid.ContextMenu>
|
||||
|
||||
<!-- 键盘 删除键绑定 Delete 命令 -->
|
||||
<!--<i:Interaction.Triggers>
|
||||
<i:EventTrigger EventName="PreviewKeyDown">
|
||||
<i:InvokeCommandAction Command="{Binding DeleteStepCommand}"
|
||||
CommandParameter="{Binding SelectedStep}"
|
||||
PassEventArgsToCommand="True" />
|
||||
</i:EventTrigger>
|
||||
</i:Interaction.Triggers>-->
|
||||
|
||||
</DataGrid>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</DockPanel>
|
||||
</GroupBox>
|
||||
|
||||
</Grid>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using DeviceCommand.Base;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
@@ -14,9 +15,72 @@ using UIShare.UIViewModel;
|
||||
|
||||
namespace UIShare.GlobalVariable
|
||||
{
|
||||
public class ScopedContext
|
||||
public class ScopedContext : BindableBase
|
||||
{
|
||||
private static readonly Random _randomSeed = new Random();
|
||||
|
||||
#region 子程序导航栈(台架隔离)
|
||||
|
||||
/// <summary>
|
||||
/// 子程序导航栈:记录从根程序进入子程序的路径。
|
||||
/// 每个台架(ScopedContext 实例)各自维护一个独立的栈,互不干扰。
|
||||
/// </summary>
|
||||
public Stack<ProgramVM> SubProgramNavigationStack { get; } = new();
|
||||
|
||||
private ProgramVM _currentProgram;
|
||||
/// <summary>
|
||||
/// 当前在 StepManager 中显示的程序(可能是根程序,也可能是某层子程序)。
|
||||
/// UI 的 DataGrid 绑定此属性,而非直接绑定 Program。
|
||||
/// </summary>
|
||||
public ProgramVM CurrentProgram
|
||||
{
|
||||
get => _currentProgram;
|
||||
set => SetProperty(ref _currentProgram, value);
|
||||
}
|
||||
|
||||
private string _breadcrumbPath = "主程序";
|
||||
/// <summary>
|
||||
/// 面包屑导航路径,如 "主程序 > 连接流程 > 初始化"
|
||||
/// </summary>
|
||||
public string BreadcrumbPath
|
||||
{
|
||||
get => _breadcrumbPath;
|
||||
set => SetProperty(ref _breadcrumbPath, value);
|
||||
}
|
||||
|
||||
private bool _canGoBack;
|
||||
/// <summary>
|
||||
/// 是否可以返回上一级(导航栈不为空时为 true)
|
||||
/// </summary>
|
||||
public bool CanGoBack
|
||||
{
|
||||
get => _canGoBack;
|
||||
set => SetProperty(ref _canGoBack, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空导航栈,回到根程序界面
|
||||
/// </summary>
|
||||
public void ResetNavigation()
|
||||
{
|
||||
SubProgramNavigationStack.Clear();
|
||||
CurrentProgram = Program;
|
||||
BreadcrumbPath = "主程序";
|
||||
CanGoBack = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据栈状态刷新 CanGoBack
|
||||
/// </summary>
|
||||
public void UpdateCanGoBack() => CanGoBack = SubProgramNavigationStack.Count > 0;
|
||||
|
||||
/// <summary>
|
||||
/// 当前导航深度(0 = 根程序,1 = 第一层子程序,以此类推)
|
||||
/// </summary>
|
||||
public int NavigationDepth => SubProgramNavigationStack.Count;
|
||||
|
||||
#endregion
|
||||
|
||||
public ProgramVM Program { get; set; } = new();
|
||||
public String SelectedStepList { get; set; } = "主程序";
|
||||
public string CurrentFilePath { get; set; }
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using UIShare.PubEvent;
|
||||
using Common.Tools;
|
||||
using Logger;
|
||||
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Model.Entity;
|
||||
using Service.Interface;
|
||||
@@ -181,7 +182,21 @@ namespace UIShare
|
||||
_scopedContext.SingleStep = false;
|
||||
}
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
|
||||
// 发布进入子程序导航事件
|
||||
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||
{
|
||||
Scope = _systemConfig.Title,
|
||||
Action = NavigateAction.Enter,
|
||||
SubProgram = step.SubProgram,
|
||||
StepName = step.Name
|
||||
});
|
||||
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
|
||||
// 发布退出子程序导航事件
|
||||
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||
{
|
||||
Scope = _systemConfig.Title,
|
||||
Action = NavigateAction.Exit
|
||||
});
|
||||
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
||||
if (SubSingleStep)
|
||||
{
|
||||
@@ -239,6 +254,7 @@ namespace UIShare
|
||||
tmpParameters.Clear();
|
||||
TestRoundID = Guid.NewGuid();
|
||||
}
|
||||
int initialLoopStackCount = loopStack.Count;
|
||||
foreach (var item in program.Parameters)
|
||||
{
|
||||
tmpParameters.TryAdd(item.ID, item);
|
||||
@@ -356,7 +372,21 @@ namespace UIShare
|
||||
_scopedContext.SingleStep = false;
|
||||
}
|
||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
|
||||
// 发布进入子程序导航事件
|
||||
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||
{
|
||||
Scope = _systemConfig.Title,
|
||||
Action = NavigateAction.Enter,
|
||||
SubProgram = step.SubProgram,
|
||||
StepName = step.Name
|
||||
});
|
||||
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
|
||||
// 发布退出子程序导航事件
|
||||
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||
{
|
||||
Scope = _systemConfig.Title,
|
||||
Action = NavigateAction.Exit
|
||||
});
|
||||
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
||||
if (SubSingleStep)
|
||||
{
|
||||
@@ -403,9 +433,15 @@ namespace UIShare
|
||||
_eventAggregator.GetEvent<RunSingalCompletedEvent>().Publish("Play");
|
||||
}
|
||||
await SaveStepRecordAsync(step, depth, false);
|
||||
// 仅检查本层执行期间压入的循环是否全部弹出,不关心父程序遗留的循环上下文
|
||||
}
|
||||
}
|
||||
bool finalResult = loopStack.Count == initialLoopStackCount && stepSuccess;
|
||||
|
||||
if (depth > 0) // 子程序
|
||||
{
|
||||
return finalResult;
|
||||
}
|
||||
return loopStack.Count == 0 && stepSuccess;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
using Prism.Events;
|
||||
using UIShare.UIViewModel;
|
||||
|
||||
namespace UIShare.PubEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// 子程序导航事件:运行时进入/退出子程序时发布,
|
||||
/// StepsManagerViewModel 订阅后自动切换显示。
|
||||
/// </summary>
|
||||
public class SubProgramNavigateEvent : PubSubEvent<SubProgramNavigatePayload>
|
||||
{
|
||||
}
|
||||
|
||||
public class SubProgramNavigatePayload
|
||||
{
|
||||
/// <summary>目标台架的作用域标识</summary>
|
||||
public string Scope { get; set; } = "";
|
||||
|
||||
/// <summary>Enter = 进入子程序,Exit = 退出回到上一级</summary>
|
||||
public NavigateAction Action { get; set; }
|
||||
|
||||
/// <summary>进入时:子程序的 ProgramVM;退出时可为 null</summary>
|
||||
public ProgramVM? SubProgram { get; set; }
|
||||
|
||||
/// <summary>子程序步骤的名称(用于面包屑显示)</summary>
|
||||
public string? StepName { get; set; }
|
||||
}
|
||||
|
||||
public enum NavigateAction
|
||||
{
|
||||
Enter,
|
||||
Exit
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user