Могу ли я оставить работника и GUI разделенными без исключений между потоками? - PullRequest
1 голос
/ 23 апреля 2020

У меня есть проект, над которым я работаю, в котором есть Windows gui (необязательно) и рабочий, который может либо записывать в gui, либо в консоль, если не указано gui. gui не является обязательным, чтобы сделать этот проект обратно совместимым с системами, которые могут не иметь среды рабочего стола (я также могу в конечном итоге переделать этот проект в конечном итоге в C или C ++, но из-за временных ограничений мне нужно что-то, чтобы работать прямо сейчас) , Большинство компьютеров, на которых программа будет работать (пока), имеют Windows XP. (Я нацеливаюсь на NET Framework 4.0.3).

Из-за того, что я хочу, чтобы gui был необязательным, я не хочу, чтобы рабочий класс жил внутри BackgroundWorker или Form. В моем реальном проекте у меня есть UserInterface «интерфейс» (c# интерфейс), который может быть реализован различными пользовательскими интерфейсами.

В Windows GUI tfhere является основным Form с кнопкой, которая открывает диалог Form. Диалог имеет многострочное текстовое поле, к которому рабочий может добавлять строки.

Поскольку я не использую BackgroundWorker или другие общепринятые способы ведения дел, я столкнулся с различными проблемами, связанными с операциями кросс-потоков и вызовом BeginInvoke до создания дескриптора окна. Мне удалось «решить» проблему дескриптора окна, по сути, вызвав _ = MainForm.Handle в конструкторе MainForm, чтобы принудительно создать создание дескриптора окна до того, как будет показано окно (чтобы рабочий мог добавлять строки в текстовое поле, что может произойти до того, как будет показан gui.

Вот мой минимальный, воспроизводимый пример, который отражает проблему, с которой я сталкиваюсь в моем реальном проекте. Проблема возникает, когда либо а) удаляется создание дескриптора окна из конструктора MainForm, что заставляет BeginInvoke жаловаться на вызов до создания дескриптора окна, либо б) как есть, когда диалоговое окно закрыл и повторно открыл вызов на ShowDialog не удается из-за операций с несколькими потоками.

Program.cs

using System;
using System.Windows.Forms;

namespace MinimalExample
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            Gui gui = new Gui();
            Worker worker = new Worker(gui);

            worker.start();
            gui.show();
        }
    }
}

Gui .cs

using System.Windows.Forms;

namespace MinimalExample
{
    class Gui
    {
        private readonly DialogForm _dialog_form;
        private readonly MainForm _main_form;

        public Gui()
        {
            _dialog_form = new DialogForm();
            _main_form = new MainForm(_dialog_form);
        }

        public void addLine(string line)
        {
            _dialog_form.addLine(line);
        }

        public void show()
        {
            Application.Run(_main_form);
        }
    }
}

MainForm.cs

using System;
using System.Windows.Forms;

namespace MinimalExample
{
    public partial class MainForm : Form
    {
        private readonly DialogForm _dialog_form;

        public MainForm(DialogForm form)
        {
            _dialog_form = form;

            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            _dialog_form.ShowDialog(this);
        }
    }
}

DialogForm.cs

using System;
using System.Windows.Forms;

namespace MinimalExample
{
    public partial class DialogForm : Form
    {
        public DialogForm()
        {
            InitializeComponent();

            _ = Handle;
            _ = textBox1.Handle;

            /* Visible = true;
               Visible = false; */
        }

        public void addLine(string line)
        {
            Action action = () =>
            {
                textBox1.AppendText(line);
                textBox1.AppendText(Environment.NewLine);
            };

            if (InvokeRequired)
                textBox1.BeginInvoke(action);
            else
                action();

        }
    }
}

Рабочий .cs

using System;
using System.Threading;

namespace MinimalExample
{
    internal class Worker
    {
        private readonly Gui _gui;
        private readonly Thread _thread;

        internal Worker(Gui gui)
        {
            _gui = gui;
            _thread = new Thread(new ThreadStart(working));
            _thread.IsBackground = true;
        }

        private void working()
        {
            while (true)
            {
                if (_gui != null)
                    _gui.addLine("Test");
                else
                    Console.WriteLine("Test");
                Thread.Sleep(1000);
            }
        }

        public void start()
        {
            _thread.Start();
        }
    }
}

Что здесь происходит? И есть ли способ, которым я могу хранить Gui и Worker отдельно, но не иметь этих исключений времени выполнения?

1 Ответ

1 голос
/ 23 апреля 2020

Часть Threading здесь не проблема. Вам просто нужно обработать создание формы немного по-другому:

Первая настройка:

  • Создание дескриптора формы можно принудительно вызвать, вызвав CreateHandle () сразу после InitializeComponent() (. Net Исходный код , связанный с вызовом метода, более интересен, также обратите внимание на вызов UpdateHandleWithOwner () ).
  • Следующим условием для отображения выходных данных рабочего потока является то, что форма также является видимой, поскольку при ее вызове также должен существовать дескриптор TextBox.
  • Класс Gui должен BeginInvoke() метод Form AddLine(), поскольку вызов метода генерируется в другом потоке (объект Gui используется потоком объекта Worker).
  • Я добавил свойство publi c в класс Gui: public bool CanWrite: класс Worker может проверить это свойство, чтобы определить, куда он должен записывать свои выходные данные.
    Свойство publi c возвращает: dialogForm != null && dialogForm.Visible;, это потому, что ↓:
  • DialogForm отображается с вызовом ShowDialog(): это означает, что когда DialogForm закрыта, форма не утилизируется. Кроме того, объект все еще имеет ссылку в классе Gui. Когда он закрыт, его свойство Visible возвращает false.

Вторая настройка:

  • Поскольку (на основе комментариев) это консольное приложение должно выводить в форму DialogForm при создании объекта Gui и диалоговое окно не обязательно отображается сразу, элемент управления TextBox в DialogForm должен кэшировать строки текста, публикуемые потоком в классе Worker.
    Это требует простого редактирования: измените dialogForm != null && dialogForm.IsVisible всего на dialogForm != null, затем проверьте состояние дескриптора перед вызовом Gui.AddLine() и кэшируйте строки текста, если дескриптор в данный момент недоступен.
  • Форма владельца, MainForm, инструктирует DialogForm воссоздавать свой дескриптор, когда возвращается DialogForm.ShowDialog(). Поскольку ShowDialog() используется для отображения DialogForm, форма не удаляется. Воссоздание дескриптора не приводит к тому, что дочерние элементы управления теряют свое содержимое.

Реализуйте обе опции:

Проверка свойства IsVisible может стать свойством Gui, что-то вроде bool UpdateOnDialogVisible, для проверки на CanWrite,, поэтому текст будет записываться в TextBox в зависимости от состояния этого свойства.


Проверено в:

- Windows 7 (у меня нет машины с WinXP)
-. Net Framework 4.0
- C# 5

In Program.cs:

class Program
{
    private static Worker worker = null;
    private static Gui gui = null;

    // [...]

    gui = new Gui();
    worker = new Worker(gui);

    worker.start();
    gui.Show();
}

In Gui .cs

public class Gui
{
    private StringBuilder sb = null;
    // [...]

    public Gui() {
        sb = new StringBuilder();
        dialogForm = new DialogForm();
        mainForm = new MainForm(dialogForm);
    }

    public bool CanWrite {
        get { return dialogForm != null }
        // Or, with the condition that the Dialog is already visible:  
        // get { return dialogForm != null && dialogForm.Visible; }
    }

    public void AddLine(string line) {
        sb.AppendLine(line);
        // Safety measure: cache if the handle is not available at this time
        if (this.CanWrite && dialogForm.IsHandleCreated) {
            dialogForm.BeginInvoke(new MethodInvoker(() => {
                dialogForm.AddLine(sb.ToString());
                sb.Clear();
            }));
        }
    }
    // [...]
}

В Worker.cs:

internal class Worker
{
    // [...]
    private void working() {
        while (true) {
            if (gui != null && gui.CanWrite) {
                gui.AddLine("Test");
            }
            else {
                Console.WriteLine("Test");
            }
            Thread.Sleep(1000);
        }
    }
    // [...]
}

В DialogForm.cs:

public partial class DialogForm : Form
{
    private TextBox textBox1;

    public DialogForm() {
        InitializeComponent();
        this.CreateHandle();
    }

    public void AddLine(string line) {
        if (this.IsDisposed || !this.IsHandleCreated) return;
        this.textBox1.AppendText(line);
        this.textBox1.ScrollToCaret();
    }

    public void RecreateWindow() {
        this.CreateHandle();
    }

    private void InitializeComponent() {
        // [...]
    }
}

В MainForm.cs:

public partial class MainForm : Form
{
    private Button button1;
    internal readonly DialogForm dialogForm = null;

    public MainForm() : this(null) { }
    public MainForm(DialogForm form) {
        dialogForm = form;
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        button1.Enabled = false;
        if (dialogForm != null) dialogForm.ShowDialog(this);
        button1.Enabled = true;
        // As describe in the notes, if a, e.g., UpdateOnDialogVisible () property is 
        // created, call this method when this property is true, to show text on this 
        // Window only when is Visible.
        dialogForm.RecreateWindow();
    }

    private void InitializeComponent() {
        // [...]
    }
}

Вот как это работает:

Первый вариант :

  • Выход консоли перенаправляется на форму только тогда, когда назначенная форма отображается в момент создания выхода:

Console and WinForms Threading 1

Второй параметр :
Консольный выход всегда направляется на указанную форму при создании класса Gui.

  • Дескриптор формы воссоздается каждый раз, когда закрывается. Поскольку ShowDialog () используется для его отображения, форма не удаляется.
  • TextBox, используемый для отображения вывода Консоли, также может кэшировать вывод, когда Форма закрыта.
  • Объект StringBuilder выступает в качестве вторичного кэша безопасности, если дескриптор Window не создан в нужное время (поскольку вызовы методов генерируются в другом потоке, принимается условие гипотетической гонки). во внимание).

Console and WinForms Threading 2

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...