How to organize a HIERARCHY of non-UI threads and get status and progress back for UI consumption

廉价感情. 提交于 2021-02-10 07:26:20

问题


enter image description here

Figure 1 - Working Demo

I'm running a winforms application and I have implemented a status / progress TreeView. It can display the status (by icon) and progress of a (possibly hierarchical) set of tasks to accomplish. My question is not about how to implement the TreeView control itself. I've got that part covered. The TreeView is merely the way the background work is going to status / progress itself to the user.

I have a set of methods that I want to run on not the main UI thread. They need to run in order. They are steps in a larger process. I could organize them into a hierarchy; that would make a nice tree structure.

Each of these methods will be represented by a node in the tree. I probably got the idea for this method of visualization from the old Sql Server DTS status panel. I still like that idea.

I want to know when each method finishes and it's outcome, and possibly a few textual statuses along the way. I also want a general mechanism that I can use to bubble up progress. I will use those to do an owner drawn progress bar in the TreeView on the node that corresponds to that method.

I've read up some on multi-threading, and also on the Task class, but don't really understand it fully. And I don't care if the solution uses that or not. But maybe that would be more elegant I don't know. It seems more straight forward than call backs but maybe you know better.

The Task Class:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml.Serialization;
using System.Linq;

namespace DeveloperWorkbench.Nodes
{
    public class Task : INode
    {
        public delegate void TaskStatusDelegate(Task sender, TaskStatus taskStatus);

        public event ProgressDelegate ProgressChanged;
        public event StatusDelegate Status;
        public event TaskStatusDelegate TaskStatusChanged;

        public Task()
        {
            _children = new List<Task>();
        }

        [XmlIgnore()]
        public bool CanHaveChildren { get; private set; }

        private List<Task> _children;
        public List<Task> Children
        {
            get
            {
                _children.ForEach(x => x.Parent = this);
                return _children;
            }
            set
            {
                _children = value;
                _children.ForEach(x => x.Parent = this);
            }
        }

        [XmlIgnore()]
        public List<string> ChildTypes { get; private set; }

        public string FullName { get; set; }

        private float _maxProgress = 0;
        [Browsable(false)]
        [XmlIgnore()]
        public float MaxProgress
        {
            get { return _maxProgress; }
            set
            {
                _maxProgress = value;
                RaiseProgress(this, Progress, MaxProgress);
            }
        }

        private Delegate _method;
        [Browsable(false)]
        [XmlIgnore()]
        public Delegate Method
        {
            get { return _method; }
            set
            {
                if (_method == value) return;
                _method = value;
                Name = Method.Method.Name;
                TypeName = Method.Method.ReflectedType.FullName;
            }
        }

        private string _name;
        [ReadOnly(true)]
        public string Name
        {
            get { return _name; }
            set
            {
                if (_name == value) return;
                _name = value;
                //Method = GetMethodByName(???, _name);
                FullName = ProperCaseToSpaces(_name);
            }
        }

        [Browsable(false)]
        [XmlIgnore()]
        public INode Parent { get; set; }

        private float _progress = 0;
        [Browsable(false)]
        [XmlIgnore()]
        public float Progress
        {
            get { return _progress; }
            set
            {
                _progress = value;
                RaiseProgress(this, Progress, MaxProgress);
            }
        }

        public List<KeyValuePair<string, object>> RelatedItems { get; set; }

        private TaskStatus _taskStatus = TaskStatus.Created;
        [Browsable(false)]
        [XmlIgnore()]
        public TaskStatus TaskStatus
        {
            get { return _taskStatus; }
            set
            {
                _taskStatus = value;
                TaskStatusChanged(this, _taskStatus);
            }
        }

        [ReadOnly(true)]
        public string TypeName { get; set; }

        public bool Visited { get; set; }

        public Task Add(Task child)
        {
            Children.Add(child);
            child.Parent = this;
            child.ProgressChanged += Child_Progress;
            return child;
        }

        private void Done(System.Threading.Tasks.Task task)
        {
            TaskStatus = TaskStatus.RanToCompletion;
        }

        public void Execute()
        {
            Progress = 0;
            TaskStatus = TaskStatus.Running;
            var systemTask = new System.Threading.Tasks.Task((Action)Method);
            systemTask.ContinueWith(Done);
            systemTask.Start();
            if (Parent != null)
                systemTask.Wait();
        }

        private static string ProperCaseToSpaces(string text)
        {
            return Regex.Replace(text, @"(\B[A-Z]+?(?=[A-Z][^A-Z])|\B[A-Z]+?(?=[^A-Z]))", " $1");
        }

        public void RaiseProgress(INode sender, float progress = 0, float maxProgress = 100)
        {
            ProgressChanged(sender, progress, maxProgress);
        }

        public void RaiseStatus(string status = "Ready")
        {
            Status(status);
        }

        public void Refresh(bool force)
        {
            throw new NotImplementedException();
        }

        public void RefreshChildren(bool force, string childType = null)
        {
            throw new NotImplementedException();
        }

        public List<KeyValuePair<string, INode>> RefreshRelatedItems(bool force)
        {
            throw new NotImplementedException();
        }

        //Usage: myTask.SetMethod(() => MyMethod(0, 40));
        public void SetMethod(Action method)
        {
            Method = method;
        }

        //Usage: myTask.SetMethod(() => MyFunction(myArgument));
        public void SetMethod<T>(Func<T> function)
        {
            Method = function;
        }

        public void SetMethod(Object target)
        {
            if (target.GetType().FullName == TypeName)
                Method = GetMethodByName(target, Name);
            else
            {
                var name = Name;
                SetMethod(() => FakeExecute(this));
                Name = name;
                TypeName = null;
            }

            foreach (var child in Children)
            {
                child.SetMethod(target);
            }
        }

        public void Child_Progress(INode sender, float progress = 0, float maxProgress = 100)
        {
            MaxProgress = _children.Sum(x => x.MaxProgress);
            Progress = _children.Sum(x => x.Progress);
        }

        public static Task Create<T>(Func<T> method, Task parent = null)
        {
            var task = InnerCreate(parent);
            task.SetMethod(method);
            return task;
        }

        public static Task Create(Action method, Task parent = null)
        {
            var task = InnerCreate(parent);
            task.SetMethod(method);
            return task;
        }

        public static Task Create(string methodName, Task parent = null)
        {
            var task = InnerCreate(parent);
            task.SetMethod(() => FakeExecute(task));
            task.Name = methodName;
            task.TypeName = null;
            return task;
        }

        private static Task InnerCreate(Task parent)
        {
            var task = new Task();
            if (parent != null)
                parent.Add(task);
            return task;
        }

        public static Task CurrentTask(Task rootTask, int stackFrame = 1)
        {
            var taskMethodName = new StackFrame(stackFrame).GetMethod().Name;
            return Find(rootTask, taskMethodName);
        }

        private static void FakeExecute(Task task)
        {
            foreach (Task child in task.Children)
            {
                child.MaxProgress = 100;
                child.Progress = 0;
                child.TaskStatus = TaskStatus.WaitingToRun;
            }
            foreach (Task child in task.Children)
            {
                child.Execute();
            }
        }

        private static Task Find(Task task, string methodName)
        {
            return task.Method.Method.Name == methodName ?
            task :
            task.Children.Select(child => Find(child, methodName)).FirstOrDefault(found => found != null);
        }

        static Delegate GetMethodByName(object target, string methodName)
        {
            var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy;
            MethodInfo method = target.GetType().GetMethod(methodName, bindingFlags);
            return method.ReturnType == typeof(void) ? Delegate.CreateDelegate(typeof(Action), target, method) : null;
        }
    }
}

The StatusList class:

using System.Drawing;
using System.Drawing.Drawing2D;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows.Forms;
using Retalix.R10.DeveloperWorkbench.Nodes;
using Retalix.R10.DeveloperWorkbench.UI.Helpers;
using Task = Retalix.R10.DeveloperWorkbench.Nodes.Task;

namespace DeveloperWorkbench.UI.Controls
{
    public partial class StatusList : UserControl
    {

        // Import the SetWindowRgn function from the user32.DLL
        // From the Unmanaged Code
        [DllImport("user32.DLL", EntryPoint = "SetWindowRgn")]
        private static extern int SetWindowRgn(int hWnd, int hRgn, int bRedraw);

        [System.Runtime.InteropServices.DllImport("Gdi32.dll", EntryPoint = "CreateRoundRectRgn")]
        private static extern System.IntPtr CreateRoundRectRgn
        (
            int nLeftRect, // x-coordinate of upper-left corner
            int nTopRect, // y-coordinate of upper-left corner
            int nRightRect, // x-coordinate of lower-right corner
            int nBottomRect, // y-coordinate of lower-right corner
            int nWidthEllipse, // height of ellipse
            int nHeightEllipse // width of ellipse
        );
        [System.Runtime.InteropServices.DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
        private static extern bool DeleteObject(System.IntPtr hObject);

        public StatusList()
        {
            InitializeComponent();
        }

        private TreeNode Add(TreeNodeCollection nodes, string text, string imageKey, object tag)
        {
            var treeNode = nodes.Add(tag.GetHashCode().ToString(), text);
            treeNode.Tag = tag;
            treeNode.ImageKey = imageKey;
            treeNode.SelectedImageKey = imageKey;
            tvTreeView.ExpandAll();
            return treeNode;
        }

        public TreeNode Add(Task task)
        {
            var nodes = tvTreeView.Nodes;
            if (task.Parent != null)
                nodes = Find(task.Parent).Nodes;
            task.TaskStatusChanged += Task_TaskStatusChanged;
            task.ProgressChanged += Task_Progress;
            var treeNode = Add(nodes, task.FullName, StatusIcon(task.TaskStatus), task);
            foreach(var child in task.Children)
            {
                Add(child);
            }
            return treeNode;
        }

        private TreeNode Find(object tag)
        {
            var treeNodes = tvTreeView.Nodes.Find(tag.GetHashCode().ToString(), true);
            if (treeNodes.Length > 0)
                return treeNodes[0];
            return null;
        }

        private string StatusIcon(System.Threading.Tasks.TaskStatus status)
        {
            switch (status)
            {
                case TaskStatus.Canceled:
                case TaskStatus.Created:
                case TaskStatus.Faulted:
                case TaskStatus.RanToCompletion:
                    return status.ToString();
                    break;
                case TaskStatus.Running:
                case TaskStatus.WaitingForChildrenToComplete:
                    return TaskStatus.Running.ToString();
                    break;
                default:
                    if (status.ToString().StartsWith("Waiting"))
                        return "Waiting";
                    break;
            }
            return "Created";
        }

        private void tvTreeView_DrawNode(object sender, DrawTreeNodeEventArgs e)
        {
            var task = (Task) e.Node.Tag;

            if ((e.State & TreeNodeStates.Selected) == TreeNodeStates.Selected)
            {
                e.Graphics.FillRectangle(SystemBrushes.Window, e.Bounds);
                //e.Graphics.DrawRectangle(SystemPens.ControlDark, e.Bounds.Left, e.Bounds.Top , e.Bounds.Width - 1, e.Bounds.Height - 1);
            }
            if(task.TaskStatus == TaskStatus.Running)
            {
                var borderBrush = new LinearGradientBrush(new Point(e.Bounds.Left + 1, e.Bounds.Top + 3), new Point(e.Bounds.Left + 1, e.Bounds.Bottom), Color.White, Color.FromArgb(200, Color.LightGray));
                var borderRectangle = new Rectangle(e.Bounds.Left + 1, e.Bounds.Top + 3, e.Bounds.Width - 10, e.Bounds.Height - 6);
                var borderGraphicsPath = RoundedRectangle.Create(borderRectangle);
                e.Graphics.FillPath(borderBrush, borderGraphicsPath);
                e.Graphics.DrawPath(Pens.DarkGray, borderGraphicsPath);
                //e.Graphics.FillRectangle(borderBrush, borderRectangle);
                //e.Graphics.DrawRectangle(pen, borderRectangle);
                if (task.Progress > 0)
                {
                    //pen.DashStyle = DashStyle.Dot;
                    var width = (task.Progress / task.MaxProgress) * (e.Bounds.Width - 11);
                    var progressRectangle = new Rectangle(e.Bounds.Left + 2, e.Bounds.Top + 4, (int)width, e.Bounds.Height - 7);
                    var progressGraphicsPath = RoundedRectangle.Create(progressRectangle, 5, RoundedRectangle.RectangleCorners.TopLeft | RoundedRectangle.RectangleCorners.BottomLeft);
                    //e.Graphics.DrawRectangle(pen, rectangle);
                    var progressBrush = new LinearGradientBrush(new Point(progressRectangle.Left, progressRectangle.Top - 1), new Point(progressRectangle.Left, progressRectangle.Bottom), Color.White, Color.LimeGreen);
                    e.Graphics.FillPath(progressBrush, progressGraphicsPath);
                    //e.Graphics.FillRectangle(progressLinearGradientBrush, progressRectangle);

                    //GraphicsPath path = RoundedRectangle.Create(rectangle);
                    //e.Graphics.DrawPath(Pens.Black, path);
                    //System.IntPtr ptrBorder = CreateRoundRectRgn(e.Bounds.Left, e.Bounds.Top, e.Bounds.Left + 50, e.Bounds.Bottom, 5, 5);
                    //try { SetWindowRgn(tvTreeView.Handle.ToInt32(), ptrBorder.ToInt32(), 1) ; }
                    //finally { DeleteObject(ptrBorder); }
                }
            }

            var textSize = e.Graphics.MeasureString(task.Name, tvTreeView.Font);
            var controlText = SystemBrushes.ControlText;
            e.Graphics.DrawString(task.Name, tvTreeView.Font, controlText, e.Bounds.Left - 1, e.Bounds.Top + e.Bounds.Height / 2f - textSize.Height / 2f);

            //if ((e.State & TreeNodeStates.Selected) == TreeNodeStates.Selected)
            //    controlText = SystemBrushes.HighlightText;
        }

        public void Task_Progress(Nodes.INode sender, float progress = 0, float maxProgress = 100)
        {
            if (IsDisposed) return;
            if (InvokeRequired)
            {
                Invoke(new ProgressDelegate(Task_Progress), sender, progress, maxProgress);
            }
            else
            {
                if (tvTreeView.IsDisposed) return;
                var treeNode = Find(sender);
                if (treeNode != null)
                {
                    tvTreeView.Invalidate(treeNode.Bounds);
                }
            }
        }

        public void Task_TaskStatusChanged(Task sender, TaskStatus taskStatus)
        {
            if (IsDisposed) return;
            if (InvokeRequired)
            {
                Invoke(new Task.TaskStatusDelegate(Task_TaskStatusChanged), sender, taskStatus);
            }
            else
            {
                if (tvTreeView.IsDisposed) return;
                var treeNode = Find(sender);
                if (treeNode != null)
                {
                    treeNode.ImageKey = StatusIcon(taskStatus);
                    treeNode.SelectedImageKey = treeNode.ImageKey;
                }
            }            
        }
    }
}

And how it's used:

using System;
using System.IO;
using System.Threading;
using System.Windows.Forms;
using Task = Retalix.R10.DeveloperWorkbench.Nodes.Task;

namespace DeveloperWorkbench.UI
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            BuildTaskHierarchy();
        }

        private Task _rootTask;
        public void BuildTaskHierarchy()
        {
            var roottaskXml = @"c:\temp\roottask.xml";
            if (File.Exists(roottaskXml))
            {
                //method hierarchy can be deserialized...
                _rootTask = (Task)Serialization.Deserialize(typeof(Task), roottaskXml);
                _rootTask.SetMethod(target: this);
            }
            else
            {
                //...or constructed from scratch
                _rootTask = Task.Create("Avert War With The Klingons");
                Task.Create(GetToTheEnterprise, _rootTask);
                var taskC = Task.Create("Kill General Chang", _rootTask);
                Task.Create(FindThatThingsTailpipe, taskC);
                Task.Create(TargetThatExplosionAndFire, taskC);
                Task.Create(ThwartCampKhitomerAssassination, _rootTask);
                Task.Create(ExplainToHighCommand, _rootTask);
                Serialization.Serialize(_rootTask, roottaskXml);
            }

            statusList1.Add(_rootTask);
        }

        private void GetToTheEnterprise()
        {
            LongOp();
        }

        private void FindThatThingsTailpipe()
        {
            LongOp();
        }

        private void TargetThatExplosionAndFire()
        {
            LongOp();
        }

        private void ThwartCampKhitomerAssassination()
        {
            LongOp();
        }

        private void ExplainToHighCommand()
        {
            LongOp();
        }

        private void LongOp()
        {
            var task = Task.CurrentTask(_rootTask, 2);
            task.MaxProgress = 100;
            for (var i = 0; i <= 50; i++)
            {
                task.Progress = i*2;
                Thread.Sleep(25);
            }

        }

        private void button1_Click(object sender, EventArgs e)
        {
            _rootTask.Execute();
        }
    }
}

I'm just posting my progress. I have tested this in my actual application and it's working. I still need a convenience function to raise progress from within any method. I am still looking for feedback about how I can reduce the instrumentation required here. I want the least invasive strategy possible. Something that watches the call chain at run time would be an awesome addition.


回答1:


The Progress class makes updating a UI with progress quite easy.

Just create a progress instance from within your UI; something that can take whatever information the background process currently has an update the UI appropriately:

Progress<Tuple<Operation, int>> progress = new Progress<Tuple<Operation, int>>();
progress.ProgressChanged += (_, info) =>
{
    TreeView node = GetTreeviewFromOperation(info.Item1);
    UpdateNodeWithProgress(node, info.Item2);
};

You can adjust that to whatever your circumstances are. Presumably the background process will have some sort of type that represents an operation, and you can map that back to the tree node that represents it. You could also pass in any other info you need to use to update the UI. If you have a lot of information to pass consider creating a new named type to represent it, rather than just using a Tuple as I did here.

Then just pass the progress to your background process, whatever that is (it could be a new thread, a task, the callback of an asynchronous method, or whatever).

//this is the work to do in the background
public static void DoWork(IProgress<Tuple<Operation, int>> progress)
{
    Thread.Sleep(1000); //placeholder for real work
    progress.Report(something, 50);
}

//start that work in a new task; call from the UI thread 
//right after creating the `Progress` instance
Task.Run(()=> DoWork(progress));

If you don't have .NET 4.5 you can create your own version of this class rather easily:

public interface IProgress<T>
{
    void Report(T data);
}

public class Progress<T> : IProgress<T>
{
    SynchronizationContext context;
    public Progress()
    {
        context = SynchronizationContext.Current
            ?? new SynchronizationContext();
    }

    public Progress(Action<T> action)
        : this()
    {
        ProgressReported += action;
    }

    public event Action<T> ProgressReported;

    void IProgress<T>.Report(T data)
    {
        var action = ProgressReported;
        if (action != null)
        {
            context.Post(arg => action((T)arg), data);
        }
    }
}



回答2:


Read up on the BackgroundWorker class. It's an old but very simple way to do background work without going into the complexities of threading.

All you got to do is create one, handle its DoWork event to perform your logic (which will run in the background) and pass progress back to the main ui thread through its ReportProgress function which you will then handle to update the ui of your tree however you like.



来源:https://stackoverflow.com/questions/20414292/how-to-organize-a-hierarchy-of-non-ui-threads-and-get-status-and-progress-back-f

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!