다음을 통해 공유


Communicating Between Classes

A very common question on the MSDN forums is where someone has one piece of code and it needs to tell some other piece of code to do something, call a method and or pass an object or value.  The purpose of this article is to explain how to do this in an elegant manner with both pieces of code totally decoupled. 
The solution follows the Publish/Subscribe pattern. 

Common Use Cases Include

In Winforms I have Form1 and I need to pass the contents of this textbox to Form2.
In WPF I am in one Window or ViewModel and I want to tell another ViewModel or Window to do something.
I am using MVVM and I wish to call some code behind from a View Model. 
E.G play a video or get some response from the user in a dialogue before continuing.

The Inelegant solutions

You can expose a public event or property from one Window or Class, retain or obtain a reference to the object and use this.
The reason these are inelegant solutions is that a dependency is introduced.
Testing is complicated because a mock or stub will be necessary and any tests involving this piece of code will run slightly slower.
Large complicated systems often have multiple such dependencies across classes.
Slower tests mean each TDD or BDD iteration will be slower - which is a bad thing.
Use this approach throughout a big system and those thousand tests will run noticeably slower. Not to mention take longer to write initially.

Another issue is that passing references can be very difficult to plumb together.
When your two pieces of code are several objects removed you need to introduce some sort of object to span between them.
One concrete real world example of this would be a menu usercontrol.
A menu is re-used across several windows and is hence factored into a usercontrol.
The main window hosts the menu and other Pages which are swapped in and out as the user navigates the system.
A button on the menu needs to communicate with any usercontrol within the current Page.
To connect these up a reference would need to be passed from usercontrol to page to Mainwindow to the menu.
Perhaps creating an event bus object which is passed around the system.
Passing references from one parent to child seems pretty simple.
Once you get several levels involved this quickly becomes clunky with lots of scope for memory leaks.
The more complicated your code base the higher the cost of maintenance.

The Elegant solution - Messaging

There are two freely available Messaging options - PRISM or MVVM Light.
Both of these allow you to send data and or trigger a method in some other class (or many classes) without creating any dependency at all between classes.
Neither the sender/publisher and the Receiver/Subscriber have to know anything about each other and neither is dependent on the other being instantiated.
You can subscribe to a message which is never sent or send a message with nothing subscribed and there will be no error, no problem.
In prism the messaging is perhaps somewhat confusingly called EventAggregator.
You can see an explanation here
The author's preference is MVVM Light primarily because the syntax is easier to understand - but the other MVVM components are also preferable.
You can read in detail on MVVM Light Messenger in this MSDN Article
Silverlight and Windows Phone developers should pay particular attention to Figure 5 in that article. WPF and Winforms developers should be reassured by the "no risk" in each entry of Figure 5 Risk of Memory Leak Without Unregistration.
On the face of it MVVM is a pattern used for XAML applications so the Winforms developer may be thinking "hang on - this is no good to me".
There is no dependency on anything in XAML or MVVM for Messenger to work.
This approach will solve your problems in Winforms, WPF, Silverlight, Windows App store, Windows phone and pretty much any sort of application where both classes are running in the one application.
Since MVVM Light is offered as a dll, this can be used in any .Net language - C#, Visual Basic etc.

Sending and Receiving Messages

Steps to do:

  • Declare a Type (Class) to identify the message and hold data. 
  • Instantiate an object of that type, and optionally fill it with data
  • Send it as a message 
  • Subscribe to and act on receiving a message of that Type 

Sample Solution

There is a sample on Technet Gallery which you can download to see this working.
For clarity, the sample sends extremely simple message containing nothing at all or just a string.  The message can contain whatever you can put in an object and this can be much more sophisticated. E.G on a window showing parent child data, a collection of Entity framework entities corresponding to the children of a clicked parent can be sent to a usercontrol displaying children.

Download the sample and spin it up using F5. You will see two windows:

As the titles say, one Window is a WPF Window and the other a Windows Forms Form.
The intent being to underline the fact that this technique works in any type of solution and even across technologies which might otherwise have some issues talking to one another and are best kept decoupled.

The WPF window contains:

  • A TextBox where you can see "Overtype This".
  • A yellow Usercontrol where you can see "TextBlock in UserControl".
  • A Button which will send the string in the TextBox to both the ViewModel bound to the WPF UserControl and the code behind of the Windows Form to be shown in a Label there.
  • There is also another button which says "Process requesting input" which will be of more interest to MVVM developers and will be covered later on in the article.

Click on the TextBox and Input a string on the WPF window and click the Send button.
Your entered string appears in the yellow area (a separate Usercontrol) under the TextBox and in the label in the Windows form.

Explanation of Code

Declaring a Type which will identity which message is being sent.

class Amessage
{
    public string TheText { get; set; }
}

Send a message - note the type <Amessage> defining which sort of a message it is:

var msg = new Amessage { TheText = this.txtEntry.Text };
Messenger.Default.Send<Amessage>(msg);

Registering for and handling the <Amessage> message in the WPF Usercontrol:

public UserControl1()
{
    InitializeComponent();
    this.WPFUCTextBlock.Text = "Textblock in UserControl";
    Messenger.Default.Register<Amessage>(this, (action) => ReceiveAMessage(action));
}
 
private void ReceiveAMessage(Amessage msg)
{
    this.WPFUCTextBlock.Text = msg.TheText;
}

Registering for and handling a message in the Windows Form:

public Form1()
{
    InitializeComponent();
    Messenger.Default.Register<Amessage>(this, (action) => ReceiveAMessage(action));
}
 
private void ReceiveAMessage(Amessage msg)
{
    this.label1.Text = msg.TheText;
}

ViewModel to Code Behind Example

Some UIElements are quite difficult to work with if you are using MVVM. Calling the Play method on a MediaElement is simple from code behind but a nuisance from a ViewModel.
Work rounds include an attached a property. This can work well but is a lot of code just to call a method.
Messaging can greatly simplify this.
Note: This sample code is not in the Technet Gallery sample.

The RelayCommand of a ViewModel sends a message and collapses a list of options revealing the media player underneath:

public RelayCommand<string> WatchVideo
{
    get;
    private set;
}
private void  ExecuteWatchVideo(string videouri)
{
    // media element is in an entirely separate usercontrol
    var msg = new  PlayVideo { URI = videouri };
    Messenger.Default.Send<PlayVideo>(msg);
    listVisibility = Visibility.Collapsed;
    RaisePropertyChanged("ListVisibility");


}

The code behind registers and acts on the message to play the video.

public MediaPlayerUC()
{
    InitializeComponent();
    Messenger.Default.Register<PlayVideo>(this, (action) => ReceivePlayVideoMessage(action));
}
private void ReceivePlayVideoMessage(PlayVideo playVideo)
{
    if (playVideo.URI != null)
    {
        Player.Source = new  Uri(playVideo.URI, UriKind.Relative);
        this.Visibility = Visibility.Visible;
        Player.Play();
    }
}

ViewModel Requiring user input

One issue when working in MVVM is that all your code is in the ViewModel decoupled from the UI.  What do you do when the ViewModel processing in Method X requires some user input of some sort?
One approach is to split the code from Method X into two methods instead. The first method (X1) is the processing up to requiring input and the second (X2) is that processing after the input is required.
Often such input is due to an edge case of some sort and under most circumstances the code can instead just call X2.

Logic:

if (SomeEdgeCase)
{
    //  Message View code behind for input - response message will call x2
}
else
{
    x2();
}

The sample illustrates this if you click on the bottom right button "Process requesting input".
A message is sent (InputRequestMessage) from the MainWindowViewModel, the code behind subscribes to that and shows InputChildWindow to request the input.
The user types in his response and clicks OK. 
The InputChildWindowViewModel then sends another message (InputResponseMessage) containing the input.
In turn the MainViewModel sets a property the Content of that bottom right button is bound to and you will see your response in the button.
In the real world application there would be much more code in both the X1 equivalent - ProcessExecute() and X2 equivalent (ReceiveInputMessage) 

Relevant code in the MainWIndowViewModel:

public RelayCommand DoProcess { get; set; }
 
 public MainWindowViewModel()
 {
     DoProcess = new RelayCommand(ProcessExecute);
     Messenger.Default.Register<InputResponseMessage>(this, (action) => ReceiveInputMessage(action));
 }
 private void ReceiveInputMessage(InputResponseMessage inp)
 {
     // Continue with processing
     ButtonContent = inp.UserInput;
 }
private void ProcessExecute()
{
    // Piece of processing gets to some point and then requires user input
    var msg = new InputRequestMessage();
    Messenger.Default.Send<InputRequestMessage>(msg);
}

MainWIndow code behind:

    Messenger.Default.Register<InputRequestMessage>(this, (action) => InputRequestMessageExecute(action));
}
 
private void InputRequestMessageExecute(InputRequestMessage msg)
{
    InputChildWindow input = new InputChildWindow();
    input.ShowDialog();
}

InputChildWindowViewModel sending the input:

    public InputChildWindowViewModel()
    {
        ReturnInput = new RelayCommand(ReturnInputExecute);
    }
    private void ReturnInputExecute()
    {
        // Response will invoke the method which requires input
        var msg = new InputResponseMessage { UserInput = UserInput };
        Messenger.Default.Send<InputResponseMessage>(msg);
    }
}