Как добавить Control в Control из другого потока без Invoke?

Я создал приложение, которое использует плагины. Плагин содержит ToolStrip, который я хочу добавить в основную панель контейнера ToolStrip (в классе Form1). Это просто container.TopToolStripPanel.Controls.Add(plugin.PluginToolStrip;, но если я хочу запускать код плагинов в отдельном потоке, это не так просто. (Я использую многопоточность для простого способа выгрузки плагина, мне нужно только убить поток плагина и удалить ToolStrip из основной формы)

Я отключил CheckForIllegalCrossThreadCalls = false;, чтобы разрешить неиспользование Invoke void. Но когда я хочу запустить container.TopToolStripPanel.Controls.Add(plugin.PluginToolStrip); из другого потока, программа выдает ArgumentException и говорит, что я не могу этого сделать.

Итак, как я могу создать архитектуру плагинов с возможностью убивать потоки плагинов? (Я хочу дать пользователю возможность легкого управления плагинами)

Я декомпилировал System.Windows.Forms.dll, чтобы увидеть, где это исключение, и я увидел:

            /// <summary>Adds the specified control to the control collection.</summary>
            /// <param name="value">The <see cref="T:System.Windows.Forms.Control" /> to add to the control collection. </param>
            /// <exception cref="T:System.Exception">The specified control is a top-level control, or a circular control reference would result if this control were added to the control collection. </exception>
            /// <exception cref="T:System.ArgumentException">The object assigned to the <paramref name="value" /> parameter is not a <see cref="T:System.Windows.Forms.Control" />. </exception>
            public virtual void Add(Control value)
            {
                if (value == null)
                {
                    return;
                }
                if (value.GetTopLevel())
                {
                    throw new ArgumentException(SR.GetString("TopLevelControlAdd"));
                }
                if (this.owner.CreateThreadId != value.CreateThreadId)
                {
                    throw new ArgumentException(SR.GetString("AddDifferentThreads")); //here!
                }
                /* [...] */
            }

тогда я думаю, что если я могу изменить this.owner.CreateThreadId, то я смогу передать это, если (if (this.owner.CreateThreadId != value.CreateThreadId)), и программа не будет генерировать исключение. В строке 6315 я увидел этот код:

internal int CreateThreadId
        {
            get
            {
                if (this.IsHandleCreated)
                {
                    int num;
                    return SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, this.Handle), out num);
                }
                return SafeNativeMethods.GetCurrentThreadId();
            }
        }

у нас есть только get, и он внутренний :(

Что я могу сделать? У Вас есть какие-то предложения? Спасибо и извините за мой плохой английский...


person Enter    schedule 04.04.2017    source источник
comment
Краткий ответ: нет, обновление пользовательского интерфейса из потока без использования безопасного механизма перекрестного потока, такого как SafeInvoke, является нет-нет и приведет к неприятным сбоям и трудным для поиска ошибкам. Этот принцип и правило применимы не только к C#/Windows, но и к Java, Android, iOS и т. д. Вам следует изучить перестройку механизмов плагинов, чтобы позволить использовать вызовы перекрестных потоков таким образом, который способствует плавности пользовательского интерфейса.   -  person t0mm13b    schedule 04.04.2017


Ответы (1)


Я отключил CheckForIllegalCrossThreadCalls = false; разрешить неиспользование Invoke void.

Это ничего не решает. Это свойство просто позволяет генерировать исключения, когда ваш код делает что-то не так, но его отключение не устраняет основные проблемы, которые призваны помочь вам избежать исключения.

Более серьезная проблема заключается в том, что объекты пользовательского интерфейса имеют «сходство с потоком». Они принадлежат определенному потоку, т. е. потоку, в котором создается их дескриптор окна, и если вы попытаетесь получить доступ к этим объектам из любого другого потока, этот доступ может завершиться ошибкой или привести к неправильной работе элемента управления.

Итак, как я могу создать архитектуру плагинов с возможностью убивать потоки плагинов? (Я хочу дать пользователю возможность легкого управления плагинами)

Убийство потоков опасно само по себе. Нет никакой гарантии, что вы сможете безопасно завершить поток, не остановив остальную часть вашего процесса или не повредив его данные. Вам может это сойти с рук, даже в большинстве случаев, но это ненадежный способ управления делами.

Теоретически, если вы решите пойти по этому пути, один из вариантов — создать элементы управления плагином в новом потоке. Затем вы должны убедиться, что этот поток является потоком STA, и вы должны предоставить этому потоку цикл сообщений, вызвав Application.Run() в этом потоке.

Однако вы все равно столкнетесь с проблемой, что элементы управления вашего плагина будут размещены в окне, принадлежащем другому потоку. Это еще одна область опасности, и может быть трудно заставить работать правильно, если вы вообще можете это сделать. Наличие окна, принадлежащего одному потоку и являющегося дочерним элементом окна, принадлежащего другому потоку, будет иметь свои собственные ловушки.

Самый надежный известный мне способ безопасно завершить код плагина — запустить этот код в собственном файле AppDomain. Затем вы можете снести AppDomain по своему желанию. Поскольку домены не могут напрямую обращаться к данным друг друга, это позволит избежать проблем, которые обычно возникают при прерывании потоков.

Однако это решение потребует некоторого прокси между доменами. Вы не сможете разместить фактическую часть пользовательского интерфейса объекта в отдельном домене. Вместо этого вам придется настроить систему, с помощью которой пользователь взаимодействует с некоторым компонентом, которым управляет ваш код, где эти взаимодействия преобразуются в некую прокси-связь с реализацией подключаемого модуля.

На самом деле это выполнимая вещь. Вероятно, поскольку вы имеете дело с панелями инструментов, взаимодействие пользователя с плагинами ограничено несколькими простыми элементами управления (такими как кнопки, меню и т. д.), и вы сможете разработать достойный API, обеспечивающий необходимый междоменный интерфейс. коммуникация. Но вы должны действительно хотеть такой уровень безопасности, а не рисковать тем, что пользователь может использовать подключаемый модуль с ошибками, который может нарушить весь процесс. То, что это можно сделать, не означает, что это будет стоить усилий.

person Peter Duniho    schedule 05.04.2017