Pergunta

Primeira pergunta aqui, olá a todos.

O requisito em que estou trabalhando é um pequeno aplicativo de teste que se comunica com um dispositivo externo em uma porta serial. A comunicação pode levar muito tempo e o dispositivo pode retornar todos os tipos de erros.

O dispositivo é muito abstraído em sua própria classe de que o thread da GUI começa a ser executado em seu próprio thread e possui as funções básicas usuais de dados de dados Open/Fechar/Leia/gravar. A GUI também é bem simples - escolha com porta com, aberta, feche, mostre leitura de dados ou erros do dispositivo, permita modificação e escreva de volta etc.

A questão é simplesmente como atualizar a GUI da classe de dispositivo? Existem vários tipos distintos de dados com os quais o dispositivo lida, então eu preciso de uma ponte relativamente genérica entre a classe GUI Form/Thread e a classe/thread de dispositivo de trabalho. Na direção da GUI para o dispositivo, tudo funciona bem com [Begin] Invoce chamadas para abrir/fechar/ler/gravar etc. em vários eventos gerados pela GUI.

Eu li o tópico Aqui (como atualizar a GUI de outro tópico em C#?) Onde é dada a suposição de que o fio da GUI e do trabalhador estão na mesma classe. As pesquisas do Google vomitam como criar um delegado ou como criar o trabalhador clássico de fundo, mas isso não é o que eu preciso, embora eles possam fazer parte da solução. Então, existe uma estrutura simples, mas genérica, que pode ser usada?

Meu nível de C# é moderado e eu tenho programado toda a minha vida profissional, dada uma pista, vou descobrir (e postar) ... Agradecemos antecipadamente por qualquer ajuda.

Foi útil?

Solução

Você pode expor um método público na sua classe de interface do usuário que a classe do dispositivo pode chamar o thread em segundo plano com todas as informações necessárias para passar para a interface do usuário. Esse método público será executado no contexto do encadeamento de segundo plano, mas, como pertence à classe da interface do usuário, agora você pode empregar qualquer uma das técnicas de reprodução de chamadas sobre as quais você leu.

Assim, o design mais simples do que seria:

  • Adicione um método à sua classe de interface do usuário (por exemplo MyUIForm) chamado algo como UpdateUI() Isso leva qualquer estrutura de dados que você estiver usando para passar os dados do dispositivo para a interface do usuário que você usa. Você pode declarar esse método em uma interface (por exemplo IUIForm), se você deseja suportar o DI/IOC mais tarde, e tenha o formulário implemente -o.
  • No thread a (o encadeamento da interface do usuário), sua classe de interface do usuário cria a classe de dispositivo, inicializa todas as configurações necessárias e inicia seu thread em segundo plano. Também passa um ponteiro para si mesmo.
  • No encadeamento B, o dispositivo coleta os dados e chama MyUIForm.UpdateUI() (ou IUIForm.UpdateUI()).
  • UpdateUI faz Invoke ou BeginInvoke como apropriado.

Observe que tem o benefício colateral de encapsular toda a interface do usuário e a lógica de apresentação na sua classe de interface do usuário. Sua classe de dispositivo agora pode se concentrar em lidar com o hardware.

Atualizar: Para abordar suas preocupações de escalabilidade -

Não importa quanto seu aplicativo cresça e quantas aulas de interface do usuário você tem, você ainda deseja cruzar o limite do encadeamento usando o BegininVoke para a classe de interface do usuário específica que deseja atualizar. (Essa aula de interface do usuário pode ser um controle específico ou a raiz de uma árvore visual específica, ela realmente não importa). O principal motivo é que, se você tiver mais de um threads de interface do usuário, deve garantir que a atualização de qualquer interface do usuário aconteça no tópico Essa interface do usuário do PartualR foi criada, devido à maneira como as mensagens do Windows e o Windows funcionam. Portanto, a lógica real de cruzar o fio limite deve ser encapsulada na camada da interface do usuário.

Sua classe de dispositivo não deve precisar se importar com o que as aulas de interface do usuário e em qual thread precisam ser atualizadas. Na verdade, eu pessoalmente tornaria o dispositivo totalmente ignorante sobre qualquer interface do usuário e apenas exporia eventos nele que diferentes classes de interface do usuário podem se inscrever.

Observe que a solução alternativa é tornar o encadeamento totalmente encapsulado na classe de dispositivo e tornar a interface do usuário ignorante sobre a existência de um fio BACground. No entanto, o cruzamento dos limites do encadeamento se torna responsabilidade da classe do dispositivo e deve estar contido em sua lógica; portanto, você não deve usar a maneira da interface do usuário de cruzar os threads. Isso também significa que sua classe de dispositivo está obrigada a um encadeamento de interface do usuário específico.

Outras dicas

Esta é uma versão com um manipulador de eventos.
É simplificado para que não haja controles da interface do usuário na forma e nenhuma propriedade na classe SerialioEventargs.

  1. Coloque seu código para atualizar a interface do usuário sob o comentário // Atualize a interface do usuário
  2. Coloque seu código para ler serial io sob o comentário // leia de serial io
  3. Adicione campos/propriedades à classe SerialioEventargs e preencher -a no método onreadCompled.
public class SerialIoForm : Form
{
    private delegate void SerialIoResultHandlerDelegate(object sender, SerialIoEventArgs args);
    private readonly SerialIoReader _serialIoReader;
    private readonly SerialIoResultHandlerDelegate _serialIoResultHandler;

    public SerialIoForm()
    {
        Load += SerialIoForm_Load;
        _serialIoReader = new SerialIoReader();
        _serialIoReader.ReadCompleated += SerialIoResultHandler;
        _serialIoResultHandler = SerialIoResultHandler;
    }

    private void SerialIoForm_Load(object sender, EventArgs e)
    {
        _serialIoReader.StartReading();
    }
    private void SerialIoResultHandler(object sender, SerialIoEventArgs args)
    {
        if (InvokeRequired)
        {
            Invoke(_serialIoResultHandler, sender, args);
            return;
        }
        // Update UI
    }
}
public class SerialIoReader
{
    public EventHandler ReadCompleated;
    public void StartReading()
    {
        ThreadPool.QueueUserWorkItem(ReadWorker); 
    }
    public void ReadWorker(object obj)
    {
        // Read from serial IO

        OnReadCompleated();
    }

    private void OnReadCompleated()
    {
        var readCompleated = ReadCompleated;
        if (readCompleated == null) return;
        readCompleated(this, new SerialIoEventArgs());
    }
}

public class SerialIoEventArgs : EventArgs
{
}

Portanto, depois de algumas pesquisas com base nas respostas acima, mais pesquisando no Google e pedindo a um colega que saiba um pouco sobre C# minha solução escolhida para o problema está abaixo. Continuo interessado em comentários, sugestões e refinamentos.

Primeiro, alguns detalhes sobre o problema, o que é realmente bastante genérico no sentido de que a GUI está controlando algo, que deve permanecer totalmente abstrato, através de uma série de eventos para cujas respostas a GUI deve reagir. Existem poucos problemas distintos:

  1. Os próprios eventos, com diferentes tipos de dados. Os eventos serão adicionados, removidos, alterados à medida que o programa evolui.
  2. Como preencher várias classes que compõem a GUI (diferentes UserControls) e as classes que abstraem o hardware.
  3. Todas as classes podem produzir e consumir eventos e devem permanecer dissociadas o máximo possível.
  4. O compilador deve detectar cacoms codificantes o máximo possível (por exemplo, um evento que envia um tipo de dados, mas um comsumer que espera outro)

A primeira parte disso são os eventos. Como a GUI e o dispositivo podem aumentar vários eventos, possivelmente com diferentes tipos de dados associados a eles, um despachante de eventos é útil. Isso deve ser genérico em eventos e dados, então:

    // Define a type independent class to contain event data
    public class EventArgs<T> : EventArgs
    {
    public EventArgs(T value)
    {
        m_value = value;
    }

    private T m_value;

    public T Value
    {
        get { return m_value; }
    }
}

// Create a type independent event handler to maintain a list of events.
public static class EventDispatcher<TEvent> where TEvent : new()
{
    static Dictionary<TEvent, EventHandler> Events = new Dictionary<TEvent, EventHandler>();

    // Add a new event to the list of events.
    static public void CreateEvent(TEvent Event)
    {
        Events.Add(Event, new EventHandler((s, e) => 
        {
            // Insert possible default action here, done every time the event is fired.
        }));
    }

    // Add a subscriber to the given event, the Handler will be called when the event is triggered.
    static public void Subscribe(TEvent Event, EventHandler Handler)
    {
        Events[Event] += Handler;
    }

    // Trigger the event.  Call all handlers of this event.
    static public void Fire(TEvent Event, object sender, EventArgs Data)
    {
        if (Events[Event] != null)
            Events[Event](sender, Data);

    }
}

Agora precisamos de alguns eventos e vindo do mundo C, eu gosto de enums, então defino alguns eventos que a GUI aumentará:

    public enum DEVICE_ACTION_REQUEST
    {
    LoadStuffFromXMLFile,
    StoreStuffToDevice,
    VerifyStuffOnDevice,
    etc
    }

Agora, em qualquer lugar dentro do escopo (namespace, normalmente) da classe estática do EventDispatcher, é possível definir um novo despachante:

        public void Initialize()
        {
        foreach (DEVICE_ACTION_REQUEST Action in Enum.GetValues(typeof(DEVICE_ACTION_REQUEST)))
            EventDispatcher<DEVICE_ACTION_REQUEST>.CreateEvent(Action);
        }

Isso cria um manipulador de eventos para cada evento na enumeração.

E consumido assinando o evento como este código no construtor do objeto de dispositivo consumidor:

        public DeviceController( )
    {
        EventDispatcher<DEVICE_ACTION_REQUEST>.Subscribe(DEVICE_ACTION_REQUEST.LoadAxisDefaults, (s, e) =>
            {
                InControlThread.Invoke(this, () =>
                {
                    ReadConfigXML(s, (EventArgs<string>)e);
                });
            });
    }

Onde o Incontrolthread.inVoke é uma classe abstrata que simplesmente envolve a chamada de invasão.

Os eventos podem ser levantados pela GUI simplesmente:

        private void buttonLoad_Click(object sender, EventArgs e)
        {
            string Filename = @"c:\test.xml";
            EventDispatcher<DEVICE_ACTION_REQUEST>.Fire(DEVICE_ACTION_REQUEST.LoadStuffFromXMLFile, sender, new EventArgs<string>(Filename));
        }

Isso tem a vantagem de que os tipos de criação e consumo de eventos não correspondam (aqui o nome do arquivo da string) o compilador resmungará.

Existem muitos aprimoramentos que podem ser feitos, mas essa é a nozes do problema. Eu estaria interessado, como disse nos comentários, especialmente se houver omissões/bugs ou deficiências gritantes. Espero que isso ajude alguém.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top