다음을 통해 공유


WinForms communicate between forms (C#)

Introduction

Communication between multiple forms can be accomplished several ways, from exposing properties and/or events with public modifiers rather than the default of private modifiers which is okay for the occasional lobbyist while this is bad practice when writing professional solution in Visual Studio. The intent in this article is to provide an easy to use set of classes to allow information to be passed between two or more forms in a Visual Studio C# WinForm project. 

Important

  • If the intent is to create more than simple communication, passing of strings or complex objects between form, instead require a more robust tool e.g. chat server, chat client look at using asynchronous socket path.
  • For novice developers, download the source for this article, build the solution and then while reading the article run the code samples which will provide a better learning experience then just reading this article.
  • DO NOT copy and paste code provided into a project before first having a decent understanding of how the code works in the provided samples as those developer with little or no time with generics and interfaces can endup in frustration implementing this code. 

Passing simple strings between two forms

The following examples will build upon each other.

Example 1

The task is to be able to pass a string between any open form. First an Interface is required which defines the events needed to pass strings between open forms.

The following Interface defines an event where a single parameter of type string. An Interface allows the code which follows to use a different Interface implementation for different project requirements.

public interface  IMessageListener
{
    void OnListen(string Message);
}

The following class provides methods to keep a list of listeners which can allow forms in a project to send and receive string data.  When a child form is open and closed several times through the life of an application RemoveListener should be used, otherwise the current application needs to perform more work then needed plus its possible for messages to not be seen in a listener.

using System.Collections.ObjectModel;
using BroadcastListener.Interfaces;
 
namespace BroadcastListener.Classes
{
    public class  Broadcaster
    {
        private readonly  Collection<IMessageListener> _listeners = 
            new Collection<IMessageListener>();
 
        public void  Broadcast(string  Message)
        {
            foreach (IMessageListener listener in _listeners)
            {
                listener.OnListen(Message);
            }
        }
 
        /// <summary>
        /// Add a Listener to the Collection of Listeners
        /// </summary>
        /// <param name="Listener"></param>
        public void  AddListener(IMessageListener Listener)
        {
            _listeners.Add(Listener);
        }
        /// <summary>
        /// Remove a Listener from the collection
        /// </summary>
        /// <param name="Listener"></param>
        public void  RemoveListener(IMessageListener Listener)
        {
 
            for (int index = 0; index < _listeners.Count; index++)
            {
                if (_listeners[index].Equals(Listener))
                {
                    _listeners.Remove(_listeners[index]);
                }
            }
        }
    }
}

The following factory class provides access to the Broadcaster above.

public static  class Factory
{
    private static  Broadcaster _broadcaster;
 
    public static  Broadcaster Broadcaster()
    {
        return _broadcaster ?? ( _broadcaster = new Broadcaster() );
    }
}

For any window/form which will participate with sending/receiving string implement IMessageListener. In the following Form1 is the main form and Form2 a child form.

public partial  class Form1 : Form , IMessageListener
.
.
public partial  class Form2 : Form, IMessageListener

Since the factory class is static use static using statements where 

using static  YourNamespace.Classes.Factory;

In the form constructor for forms which will participate with sending or receiving a string add. 

Broadcaster().AddListener(this);

Once adding the implementation above Visual Studio will complain, allow Visual Studio to implement OnListen. In OnListen in Form1 clear out the not implemented code. In form2, 

The following is taken directly from one of the attached code samples. When OnListen is triggered the string passed will be set to an existing TextBox.

public void  OnListen(string  Message)
{
    FromForm2TextBox.Text = Message;
}

To send a string use

Broadcaster().Broadcast(SomeTextBox.Text);

Any forms which are participating in broadcasting and have code in the OnListener event will have access to the string sent. There is an inherent issue, suppose form1 sends a string intended for form2? There is no way to know were the string came from. This disallows passing information back and forth between forms and know if the string was intended for that form (the current form). Example 2 will build on this example to allow a form to know the caller.

Example 2

This example builds on the latter where there was no indicator on knowing were a message came from. In the last example the Interface had one event with a string parameter. This Interface requires text and a form.

public interface  IMessageListener1 
{
    void OnListen(string Message, Form Type);
}

Broadcaster class changes using the above Interface. Besides changing the Interface Broadcast method needs the form type defined in IMessageListener1.

public class  Broadcaster
{
    private readonly  Collection<IMessageListener1> _listeners = 
        new Collection<IMessageListener1>();
 
    /// <summary>
    /// Send message
    /// </summary>
    /// <param name="message">Message</param>
    /// <param name="sender"></param>
    /// <remarks></remarks>
    [DebuggerStepThrough()]
    public void  Broadcast(string  message, Form sender)
    {
        foreach (IMessageListener1 listener in _listeners)
        {
            listener.OnListen(message, sender);
        }
    }
    
    /// <summary>
    /// Add a Listener to the Collection of Listeners
    /// </summary>
    /// <param name="listener"></param>
    public void  AddListener(IMessageListener1 listener)
    {
        _listeners.Add(listener);
    }
    /// <summary>
    /// Remove a Listener from the collection
    /// </summary>
    /// <param name="listener"></param>
    public void  RemoveListener(IMessageListener1 listener)
    {
 
        for (int index = 0; index < _listeners.Count; index++)
        {
            if ( _listeners[index].Equals(listener) )
            {
                _listeners.Remove(_listeners[index]);
            }
        }
    }
}

Implementing the new Interface for each form

public partial  class SomeForm : Form, IMessageListener1

In OnListen determine if the message is worth looking at. In the following example the form is a child form called from the main form. If the message/string came from Form1 then assign the string to a TextBox, otherwise the string came from the current form and not intended for this form.

public void  OnListen(string  Message, Form sender)
{
    if (sender is Form1)
    {
        StringFromForm1TextBox.Text = Message;
    }
}

Likewise, in the main form only react to strings from the child form.

public void  OnListen(string  message, Form sender)
{
 
    if (sender is Form2)
    {
        FromForm2TextBox.Text = message;
    }
 
}

To send a string from a form where the second parameter is the current form.

Broadcaster().Broadcast(SimpleMessageToChildTextBox.Text, this);

Passing class instances between two forms.

In the following example a person object will be used for passing to another form.

public class  Person
{
    public int  Id { get; set; }
    public string  FirstName { get; set; }
    public string  LastName { get; set; }
    public override  string ToString()
    {
        return $"{FirstName} {LastName}";
    }
}

The Interface from the last example first parameter was a string, in this example a person is the first parameter, second parameter remain the same.

public interface  IMessageListener
{
    void OnListen(Person person, Form form);
}

No changes to the Factory class

public static  class Factory
{
    private static  Broadcaster _broadcaster;
 
    public static  Broadcaster Broadcaster()
    {
        return _broadcaster ?? ( _broadcaster = new Broadcaster() );
    }
}

The Broadcaster class uses the same Interface name but with the first parameter a Person, not a string, this is easy enough to change.

In this example form1 (main form) displays a child form modally, each time the add button is clicked or pressed values of two TextBox controls are broadcasted to all listeners,

Broadcaster().Broadcast(new Person()
{
    FirstName = FirstNameTextBox.Text,
    LastName = LastNameTextBox.Text,
    Id = -1
}, this);

In the child form OnListen event if the form is the main form assign two TextBox.Text properties to values passed.

public void  OnListen(Person person, Form form)
{
    if (form is Form1)
    {
        FirstNameTextBox.Text = person.FirstName;
        LastNameTextBox.Text = person.LastName;
    }
}

Child form created, broadcast a person then shows the child form.

private void  AddPeopleButton_Click(object sender, EventArgs e)
{
    var form = new  AddPeopleForm();
 
    Broadcaster().Broadcast(new Person()
    {
        FirstName = "Jane",
        LastName = "Adams",
        Id = -1
    }, this);
 
    form.ShowDialog();
}

Custom controls

Several custom controls are used in this code sample worth mentioning, ObserableButton provides events via implementing

IObservable<string>

With its own event for passing, in this case string information via Notify event.

public void  Notify(string  Message)
{
 
    foreach (IObserver<string> observer in  _observers)
    {
        observer.OnNext(Message);
    }
 
}

Which works by simply passing a string to Notify of an instance of this button. For this code sample a custom Textbox, ObserverTextBox implements the same interface as the button above.

public class  ObserverTextBox : TextBox, IObserver<string>
{
    private IDisposable _unsubscriber;
 
    void IObserver<string>.OnCompleted()
    {
    }
 
    void IObserver<string>.OnError(Exception error)
    {
 
    }
    void IObserver<string>.OnNext(string value)
    {
        Text = value;
        Refresh();
    }
 
    public virtual  void Subscribe(IObservable<string> provider)
    {
        if (provider != null)
        {
            _unsubscriber = provider.Subscribe(this);
        }
    }
    public virtual  void Unsubscribe()
    {
        _unsubscriber.Dispose();
    }
}

Rather than passing a string around in this code sample a Reset method is used to clear TextBoxes which have subscribed to the button above.

FirstNameTextBox.Subscribe(AddButton);
LastNameTextBox.Subscribe(AddButton);

When its appropriate use Reset to clear those TextBoxes.

AddButton.Reset();

See also

Event handling in an MVVM WPF application
Windows forms custom delegates and events
Simple Multi-User TCP/IP Client/Server using TAP

Summary

Code has been presented to provide an easy way to pass information between two or more forms in a single project starting off with the very basics and concluding with an example using a class object. Other options include delegate/events and sockets (see the following included). There is not one correct path for every task requiring sharing information between forms, have a understanding of what has been presented along with sockets, delegates and events. When transitioning to WPF working with information between windows has classes and Interfaces e.g. DependencyProperty class and ObservableCollection<T> class.

Source code

GitHub repository (include a single visual basic code sample along with three C# code samples)