WPF MVVM 进阶系列教程(二、数据验证),结合之前讨论的半导体生产计划、最小公倍数(LCM)、优化算法(如 PSO、DFS、BFS)、连通分量、图形化树绘制器、异步编程和分布式计算框架(如 MPI),我理解您希望在 WPF MVVM 架构中实现数据验证,应用于半导体生产计划的调度优化场景(如验证工序周期、启动时间或设备分配输入),并可能与异步任务调度和图形化树绘制结合,确保用户输入合法且界面响应流畅。本教程将:

  1. 详细说明 WPF MVVM 中的数据验证机制,包括 IDataErrorInfo、INotifyDataErrorInfo 和 ValidationRule。
  2. 提供 C# 实现,结合 MVVM 模式、数据验证、异步 PSO 优化、LCM 计算和 WPF 图形化树绘制。
  3. 展示测试用例,验证数据验证在多工序调度中的效果。
  4. 说明如何与连通分量、异步编程和 MPI 集成。

如果您需要特定验证场景(如验证工序周期范围、依赖关系)或更复杂的 MVVM 功能(如命令绑定、动态验证规则),请进一步说明!


一、WPF MVVM 数据验证原理

1.1 数据验证概述在 WPF MVVM 架构中,数据验证确保用户输入或计算结果(如工序周期、启动时间)符合业务规则,防止无效数据影响调度优化或图形化可视化。验证通常在 ViewModel 层实现,与 View(XAML)绑定,提供实时错误反馈。常见验证机制:

  1. IDataErrorInfo:
    • 传统接口,提供属性级验证,适合简单场景。
    • 缺点:不支持异步验证或多错误反馈。
  2. INotifyDataErrorInfo:
    • 现代接口,支持异步验证、多个错误和动态通知。
    • 适合复杂场景,如半导体调度中的动态周期验证。
  3. ValidationRule:
    • XAML 绑定规则,适合 UI 层验证(如输入框格式)。
    • 常与 IDataErrorInfo 或 INotifyDataErrorInfo 结合。

半导体场景中的应用:

  • 验证工序周期:确保周期为正整数,避免 LCM 计算异常。
  • 验证启动时间:确保时间在 LCM 范围内,符合 PSO 优化约束。
  • 验证依赖关系:确保任务依赖图无环,适合连通分量分解。
  • 异步验证:验证 MES/EAP 数据(如设备状态)时,异步查询数据库。

1.2 数据验证在 MVVM 中的实现

  • View:XAML 定义输入控件(如 TextBox),绑定 ViewModel 属性,显示验证错误。
  • ViewModel:实现 IDataErrorInfo 或 INotifyDataErrorInfo,验证输入并通知 View。
  • Model:存储业务数据(如工序周期、启动时间),与优化算法交互。
  • 异步验证:结合 async/await,验证 MES 数据或 PSO 结果。

1.3 验证最佳实践

  • 实时反馈:绑定时使用 ValidatesOnDataErrors 和 NotifyOnValidationError,实时显示错误。
  • 异步验证:使用 INotifyDataErrorInfo 支持异步数据库查询。
  • 错误模板:自定义 XAML 错误样式(如红框提示)。
  • 命令绑定:禁用命令(如“优化”按钮)直到验证通过。

二、C# 实现(WPF MVVM 数据验证)以下实现基于 MVVM 模式,结合 INotifyDataErrorInfo 实现数据验证,应用于半导体多工序调度优化。包含:

  • 数据验证:验证工序周期和启动时间。
  • 异步 PSO 优化:并行优化调度,结合 LCM。
  • 图形化树绘制:异步绘制调度树,显示优化结果。
  • MVVM 架构:ViewModel 处理验证和优化,View 显示输入和树。

2.1 Model(ProductionScheduler.cs)csharp

 

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

public class ProductionScheduler
{
    // GCD 和 LCM 计算
    public static int GCD(int a, int b)
    {
        if (a < 0 || b < 0) throw new ArgumentException("输入必须为非负数。");
        while (b != 0) (a, b) = (b, a % b);
        return a;
    }

    public static long LCM(int a, int b)
    {
        if (a == 0 || b == 0) throw new ArgumentException("输入不能为零。");
        return Math.Abs((long)a * b) / GCD(a, b);
    }

    // 异步并行计算 LCM
    public static async Task<long> ParallelLCMAsync(IEnumerable<int> numbers, CancellationToken cancellationToken = default)
    {
        if (!numbers.Any()) throw new ArgumentException("输入不能为空。");
        var numArray = numbers.ToArray();
        if (numArray.Length == 1) return numArray[0];

        var pairs = numArray
            .Select((n, i) => new { Number = n, Index = i })
            .GroupBy(x => x.Index / 2)
            .Select(g => g.Select(x => x.Number).ToArray())
            .AsParallel()
            .WithDegreeOfParallelism(Environment.ProcessorCount)
            .WithCancellation(cancellationToken)
            .Select(pair => pair.Length == 2 ? LCM(pair[0], pair[1]) : pair[0])
            .ToList();

        return await Task.Run(() => pairs.Aggregate((a, b) => (int)LCM(a, b)), cancellationToken).ConfigureAwait(false);
    }

    // 树节点类
    public class TreeNode
    {
        public string Name { get; set; }
        public double Value { get; set; }
        public List<TreeNode> Children { get; set; }

        public TreeNode(string name, double value)
        {
            Name = name;
            Value = value;
            Children = new List<TreeNode>();
        }

        public void AddChild(TreeNode child)
        {
            Children.Add(child);
        }
    }

    // 异步 PSO 优化
    public static async Task<(double[], double)> ParallelPSOAsync(int[] processCycles, long syncCycle, CancellationToken cancellationToken = default)
    {
        var random = new Random();
        var bestSolutions = new ConcurrentBag<(double[], double)>();

        await Task.Run(() =>
        {
            Parallel.For(0, Environment.ProcessorCount * 2, new ParallelOptions { CancellationToken = cancellationToken }, i =>
            {
                double[] position = new double[processCycles.Length];
                double[] velocity = new double[processCycles.Length];
                double[] pBestPosition = new double[processCycles.Length];
                double pBestFitness = double.MaxValue;

                for (int j = 0; j < position.Length; j++)
                {
                    position[j] = random.NextDouble() * syncCycle;
                    velocity[j] = random.NextDouble() * 10 - 5;
                    pBestPosition[j] = position[j];
                }

                for (int iter = 0; iter < 100; iter++)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    double fitness = position.Zip(processCycles, (s, c) => s + c).Max();
                    if (fitness < pBestFitness)
                    {
                        pBestFitness = fitness;
                        Array.Copy(position, pBestPosition, position.Length);
                    }

                    for (int j = 0; j < position.Length; j++)
                    {
                        velocity[j] = 0.7 * velocity[j] + 2.0 * random.NextDouble() * (pBestPosition[j] - position[j]);
                        position[j] = Math.Max(0, Math.Min(syncCycle, position[j] + velocity[j]));
                    }
                }
                bestSolutions.Add((pBestPosition, pBestFitness));
            });
        }, cancellationToken).ConfigureAwait(false);

        var bestSolution = bestSolutions.OrderBy(s => s.Item2).FirstOrDefault();
        return bestSolution.Item1 != null ? bestSolution : (new double[processCycles.Length], double.MaxValue);
    }

    // 构建调度树
    public static TreeNode BuildScheduleTree(int[] processCycles, double[] startTimes)
    {
        double makespan = startTimes.Zip(processCycles, (s, c) => s + c).Max();
        var root = new TreeNode("Production Plan", makespan);
        for (int i = 0; i < processCycles.Length; i++)
        {
            var processNode = new TreeNode($"Process {i + 1}", processCycles[i]);
            processNode.AddChild(new TreeNode("Start Time", startTimes[i]));
            processNode.AddChild(new TreeNode("Completion Time", startTimes[i] + processCycles[i]));
            root.AddChild(processNode);
        }
        return root;
    }
}

2.2 ViewModel(SchedulerViewModel.cs)csharp

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;

public class SchedulerViewModel : INotifyPropertyChanged, INotifyDataErrorInfo
{
    private readonly Dictionary<string, List<string>> _errors = new Dictionary<string, List<string>>();
    private string _cyclesInput;
    private bool _isOptimizing;
    private ProductionScheduler.TreeNode _scheduleTree;
    private CancellationTokenSource _cts = new CancellationTokenSource();

    public event PropertyChangedEventHandler PropertyChanged;
    public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;

    public string CyclesInput
    {
        get => _cyclesInput;
        set
        {
            if (_cyclesInput != value)
            {
                _cyclesInput = value;
                ValidateCyclesInput();
                OnPropertyChanged();
            }
        }
    }

    public bool IsOptimizing
    {
        get => _isOptimizing;
        set
        {
            if (_isOptimizing != value)
            {
                _isOptimizing = value;
                OnPropertyChanged();
                OptimizeCommand.RaiseCanExecuteChanged();
            }
        }
    }

    public ProductionScheduler.TreeNode ScheduleTree
    {
        get => _scheduleTree;
        set
        {
            _scheduleTree = value;
            OnPropertyChanged();
        }
    }

    public ICommand OptimizeCommand { get; }
    public ICommand CancelCommand { get; }

    public SchedulerViewModel()
    {
        OptimizeCommand = new RelayCommand(async () => await OptimizeAsync(), () => !IsOptimizing && !HasErrors);
        CancelCommand = new RelayCommand(() => _cts.Cancel());
        CyclesInput = "20,30"; // 默认值
    }

    // 数据验证
    private void ValidateCyclesInput()
    {
        _errors.Remove(nameof(CyclesInput));
        var errors = new List<string>();

        if (string.IsNullOrWhiteSpace(CyclesInput))
        {
            errors.Add("周期输入不能为空。");
        }
        else
        {
            try
            {
                var cycles = CyclesInput.Split(',').Select(int.Parse).ToArray();
                if (cycles.Any(c => c <= 0))
                    errors.Add("所有周期必须为正整数。");
                if (cycles.Length < 1)
                    errors.Add("至少需要一个周期。");
            }
            catch
            {
                errors.Add("请输入以逗号分隔的整数(如 20,30)。");
            }
        }

        if (errors.Any())
            _errors[nameof(CyclesInput)] = errors;
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(CyclesInput)));
    }

    // 异步优化
    private async Task OptimizeAsync()
    {
        try
        {
            IsOptimizing = true;
            _cts = new CancellationTokenSource();
            var cycles = CyclesInput.Split(',').Select(int.Parse).ToArray();
            long syncCycle = await ProductionScheduler.ParallelLCMAsync(cycles, _cts.Token).ConfigureAwait(false);
            var (startTimes, makespan) = await ProductionScheduler.ParallelPSOAsync(cycles, syncCycle, _cts.Token).ConfigureAwait(false);
            ScheduleTree = ProductionScheduler.BuildScheduleTree(cycles, startTimes);
        }
        catch (OperationCanceledException)
        {
            // 取消处理
        }
        catch (Exception ex)
        {
            _errors[nameof(CyclesInput)] = new List<string> { $"优化失败:{ex.Message}" };
            ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(CyclesInput)));
        }
        finally
        {
            IsOptimizing = false;
        }
    }

    // INotifyDataErrorInfo 实现
    public bool HasErrors => _errors.Any();

    public IEnumerable GetErrors(string propertyName)
    {
        return _errors.TryGetValue(propertyName ?? "", out var errors) ? errors : Enumerable.Empty<string>();
    }

    // INotifyPropertyChanged 实现
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

// RelayCommand 实现
public class RelayCommand : ICommand
{
    private readonly Action _execute;
    private readonly Func<bool> _canExecute;

    public RelayCommand(Action execute, Func<bool> canExecute = null)
    {
        _execute = execute;
        _canExecute = canExecute;
    }

    public event EventHandler CanExecuteChanged
    {
        add => CommandManager.RequerySuggested += value;
        remove => CommandManager.RequerySuggested -= value;
    }

    public bool CanExecute(object parameter) => _canExecute?.Invoke() ?? true;

    public void Execute(object parameter) => _execute();

    public void RaiseCanExecuteChanged() => CommandManager.InvalidateRequerySuggested();
}

2.3 View(MainWindow.xaml)xaml

 

<Window x:Class="TreeDrawerWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Tree Drawer" Height="600" Width="800"
        DataContext="{Binding RelativeSource={RelativeSource Self}, Path=ViewModel}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <!-- 输入和控制 -->
        <StackPanel Grid.Row="0" Margin="10">
            <TextBlock Text="工序周期(以逗号分隔,如 20,30)"/>
            <TextBox Text="{Binding CyclesInput, ValidatesOnDataErrors=True, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}"/>
            <TextBlock Foreground="Red" Margin="0,5,0,0">
                <TextBlock.Text>
                    <MultiBinding StringFormat="{}{0}">
                        <Binding Path="(Validation.Errors)[0].ErrorContent" ElementName="CyclesInputTextBox"/>
                    </MultiBinding>
                </TextBlock.Text>
            </TextBlock>
            <StackPanel Orientation="Horizontal" Margin="0,10,0,0">
                <Button Content="Optimize" Command="{Binding OptimizeCommand}" Width="100" Margin="0,0,10,0"/>
                <Button Content="Cancel" Command="{Binding CancelCommand}" Width="100"/>
            </StackPanel>
        </StackPanel>

        <!-- 树绘制区域 -->
        <Canvas x:Name="TreeCanvas" Grid.Row="1" Background="White"/>
    </Grid>
</Window>

2.4 View 代码隐藏(MainWindow.xaml.cs)csharp

 

using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

namespace TreeDrawerWPF
{
    public partial class MainWindow : Window
    {
        public SchedulerViewModel ViewModel { get; } = new SchedulerViewModel();

        public MainWindow()
        {
            InitializeComponent();
            DataContext = ViewModel;

            // 监听树变化并绘制
            ViewModel.PropertyChanged += async (s, e) =>
            {
                if (e.PropertyName == nameof(ViewModel.ScheduleTree) && ViewModel.ScheduleTree != null)
                {
                    TreeCanvas.Children.Clear();
                    await DrawTreeAsync(ViewModel.ScheduleTree, 400, 50, 200, 100, CancellationToken.None);
                }
            };
        }

        // 异步绘制树
        private async Task DrawTreeAsync(ProductionScheduler.TreeNode node, double x, double y, double xOffset, double yOffset, CancellationToken cancellationToken)
        {
            if (node == null) return;
            cancellationToken.ThrowIfCancellationRequested();

            await Dispatcher.InvokeAsync(() =>
            {
                var rect = new Rectangle
                {
                    Width = 120,
                    Height = 40,
                    Fill = Brushes.LightBlue,
                    Stroke = Brushes.Black,
                    StrokeThickness = 1
                };
                Canvas.SetLeft(rect, x - 60);
                Canvas.SetTop(rect, y - 20);
                TreeCanvas.Children.Add(rect);

                rect.MouseLeftButtonDown += (s, e) =>
                {
                    MessageBox.Show($"{node.Name}: {node.Value:F2}");
                };

                var text = new TextBlock
                {
                    Text = $"{node.Name}: {node.Value:F2}",
                    FontSize = 12,
                    Foreground = Brushes.Black
                };
                Canvas.SetLeft(text, x - 50);
                Canvas.SetTop(text, y - 10);
                TreeCanvas.Children.Add(text);

                double childX = x - (node.Children.Count - 1) * xOffset / 2;
                for (int i = 0; i < node.Children.Count; i++)
                {
                    var child = node.Children[i];
                    double childY = y + yOffset;
                    DrawTreeAsync(child, childX + i * xOffset, childY, xOffset / 2, yOffset, cancellationToken).GetAwaiter().GetResult();

                    var line = new Line
                    {
                        X1 = x,
                        Y1 = y + 20,
                        X2 = childX + i * xOffset,
                        Y2 = childY - 20,
                        Stroke = Brushes.Black,
                        StrokeThickness = 2
                    };
                    TreeCanvas.Children.Add(line);
                }
            }, System.Windows.Threading.DispatcherPriority.Background, cancellationToken);
        }
    }
}

实现说明:

  • 项目设置:创建 WPF 项目,添加上述代码,需 .NET 6 或更高版本。
  • MVVM 架构:
    • Model:ProductionScheduler 提供 LCM 计算、PSO 优化和树构建。
    • ViewModel:SchedulerViewModel 实现 INotifyDataErrorInfo,验证工序周期,管理优化命令。
    • View:XAML 定义输入框、按钮和 Canvas,绑定 ViewModel 属性,显示验证错误。
  • 数据验证:
    • 验证工序周期(正整数、逗号分隔),实时显示错误。
    • OptimizeCommand 仅在无错误时启用。
  • 异步优化:异步计算 LCM 和 PSO,结合 CancellationToken。
  • 图形化树绘制:异步绘制调度树,支持点击交互。

三、测试用例运行 WPF 程序验证 MVVM 数据验证效果:

  1. 测试用例 1:有效输入
    • 输入:CyclesInput = "20,30"
    • 操作:点击“Optimize”按钮。
    • 预期输出:
      • 窗口标题:Production Plan (LCM: 60 min, Makespan: ~30.00 min)
      • 图形化树:根节点 Production Plan,子节点 Process 1 和 Process 2。
      • 点击节点显示详情(如 Process 1: 20.00)。
      • 无验证错误,按钮启用。
  2. 测试用例 2:无效输入
    • 输入:CyclesInput = "0,30"
    • 操作:输入后检查验证。
    • 预期输出:
      • 错误提示:所有周期必须为正整数。
      • “Optimize”按钮禁用。
  3. 测试用例 3:格式错误
    • 输入:CyclesInput = "20,a"
    • 操作:输入后检查验证。
    • 预期输出:
      • 错误提示:请输入以逗号分隔的整数(如 20,30)。
      • “Optimize”按钮禁用。
  4. 测试用例 4:取消优化
    • 输入:CyclesInput = "20,30"
    • 操作:点击“Optimize”,然后点击“Cancel”。
    • 预期输出:优化终止,无树绘制。
  5. 测试用例 5:更多工序
    • 输入:CyclesInput = "15,25,30"
    • 操作:点击“Optimize”。
    • 预期输出:
      • 窗口标题:Production Plan (LCM: 150 min, Makespan: ~30.00 min)
      • 图形化树展示三个工序。

四、数据验证详细说明

  1. INotifyDataErrorInfo:
    • 实现实时验证,动态通知错误(如非正整数周期)。
    • 使用 _errors 字典存储属性错误,支持多错误反馈。
    • ErrorsChanged 事件触发 XAML 更新错误提示。
  2. XAML 绑定:
    • ValidatesOnDataErrors=True 和 NotifyOnValidationError=True 启用验证。
    • MultiBinding 显示首个错误,红字提示用户。
  3. 命令绑定:
    • OptimizeCommand 仅在 HasErrors=false 时启用。
    • CancelCommand 使用 CancellationTokenSource 终止优化。
  4. 异步集成:
    • ParallelLCMAsync 和 ParallelPSOAsync 使用 ConfigureAwait(false) 优化性能。
    • DrawTreeAsync 异步绘制,保持 UI 响应。
  5. 半导体场景:
    • 验证工序周期,确保 PSO 和 LCM 计算合法。
    • 图形化树展示优化结果,支持交互。

五、实际效果

  1. 多工序调度:
    • 输入 20,30,验证通过,LCM = 60 分钟,Makespan ≈ 30 分钟。
    • 图形化树显示调度方案,交互流畅。
  2. 无效输入:
    • 输入 0,30 或 20,a,显示错误提示,禁用优化按钮。
  3. 性能优势:
    • 数据验证实时反馈,防止无效输入。
    • 异步优化和绘制,保持 UI 响应,适合 MES/EAP。

六、扩展与优化建议

  1. 高级验证:
    • 添加异步验证:如查询 MES 数据库验证周期有效性。
    • 实现 ValidationRule:在 XAML 中验证输入格式。
  2. 集成连通分量/DFS/BFS:
    • 验证依赖图无环,结合连通分量分解。
    • 并行运行 DFS/BFS,异步优化每组分量。
  3. MPI 集成:
    • 使用 RunMPIScriptAsync 调用 Python mpi4py 脚本,分布式优化 PSO。
    • 验证 MPI 输出,确保周期和启动时间合法。
  4. 增强交互性:
    • 支持动态添加工序输入,使用 ObservableCollection。
    • 添加拖动、缩放树功能,使用 WPF ScaleTransform。
  5. 集成 MES/EAP:
    • 异步从 MES/EAP 获取周期,验证后优化。
    • 导出树为 JSON,集成到 MES 仪表板。
  6. 跨平台实现:
    • 使用 Avalonia 替代 WPF,实现跨平台 UI。
    • 结合 C/C++ MPI 动态库,优化分布式计算。

七、总结WPF MVVM 数据验证通过 INotifyDataErrorInfo 实现实时输入验证,确保半导体生产计划中的工序周期合法,结合异步 PSO 优化、LCM 计算和图形化树绘制,构建高效调度系统。上述实现支持实时错误反馈、异步优化和交互式树可视化,适合 MES/EAP 应用。测试用例验证了验证和性能效果。如果您需要:

  • 特定验证规则(如依赖关系验证)。
  • 更复杂 MVVM 功能(如动态绑定、命令参数)。
  • 集成 MPI 或其他框架。 请提供具体需求,我将进一步优化代码和说明

 

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐