简体   繁体   中英

MVVM + Mediator pattern: Registration of Mediator occurs too late

I have tried to implement Mediator Pattern in WPF/MVVM app to make possible the communication between ViewModels.

To apply mediator pattern I downloaded a sample project from this link . And then I learned it from the sample and then I applied to my Sample Project.

I have got some problems with the usage of this pattern which in-turn produces ridiculous output.

Let me start this with my code:

Here is my project Structure:

SampleWPFMVVMMediatorApp
|
|--Data
|  |--MenuItems.xml
|
|--Extensions
|  |--MediatorX
|  |  |--IColleague.cs
|  |  |--Mediator.cs
|  |  |--Messages.cs
|  |  |--MultiDictionary.cs
|  |--ViewModelBase.cs
|
|--Models
|  |--MenuItem.cs
|
|--ViewModels
|  |--MainWindowViewModel.cs
|  |--ParentMenuViewModel.cs
|  |--ChildMenuViewModel.cs
|  |--SamplePageViewModel.cs
|
|--Views
|  |--ParentMenuView.xaml
|  |--ChildMenuView.xaml
|  |--SamplePage.xaml
|
|--App.xaml
|--MainWindow.xaml

Code:

I will just post the code for ViewModels and Models to shorten the length of the question.

MenuItem.cs

public class MenuItem 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

MainWindowViewModel.cs

public class MainWindowViewModel : ViewModelBase
{
    public MainWindowViewModel()
    {
        Mediator.Register(this, new[] { Messages.SelectedParentMenuItem, Messages.SelectedChildMenuItem });
    }

    private string _sourcePage;
    public string SourcePage
    {
        get
        {
            return _sourcePage;
        }
        set
        {
            _sourcePage = value;
            NotifyPropertyChanged("SourcePage");
        }
    }

    private MenuItem _currentParentMenuItem;
    public MenuItem CurrentParentMenuItem
    {
        get
        {
            return _currentParentMenuItem;
        }
        set
        {
            _currentParentMenuItem = value;
            NotifyPropertyChanged("CurrentParentMenuItem");
        }
    }

    private MenuItem _currentChildMenuItem;
    public MenuItem CurrentChildMenuItem
    {
        get
        {
            return _currentChildMenuItem;
        }
        set
        {
            _currentChildMenuItem = value;
            NotifyPropertyChanged("CurrentChildMenuItem");

            if (CurrentChildMenuItem != null)
            {
                SourcePage = (from menuItem in XDocument.Load(Messages.DataDirectory + "MenuItems.xml")
                                                        .Element("MenuItems").Elements("MenuItem").Elements("MenuItem")
                              where (int)menuItem.Parent.Attribute("Id") == CurrentParentMenuItem.Id &&
                                    (int)menuItem.Attribute("Id") == CurrentChildMenuItem.Id
                              select menuItem.Element("SourcePage").Value).FirstOrDefault();
            }
        }
    }

    public override void MessageNotification(string message, object args)
    {
        switch (message)
        {
            case Messages.SelectedParentMenuItem:
                CurrentParentMenuItem = (MenuItem)args;
                break;
            case Messages.SelectedChildMenuItem:
                CurrentChildMenuItem = (MenuItem)args;
                break;
        }
    }
}

ParentMenuViewModel.cs

public class ParentMenuViewModel : ViewModelBase
{
    public ParentMenuViewModel()
    {
        ParentMenuItems = new ObservableCollection<MenuItem>(
                                                                from menuItem in XDocument.Load(Messages.DataDirectory + "MenuItems.xml")
                                                                                          .Element("MenuItems").Elements("MenuItem")
                                                                select new MenuItem
                                                                {
                                                                    Id = Convert.ToInt32(menuItem.Attribute("Id").Value),
                                                                    Name = menuItem.Element("Name").Value
                                                                }
                                                            );
    }

    private ObservableCollection<MenuItem> _parentMenuItems;
    public ObservableCollection<MenuItem> ParentMenuItems
    {
        get
        {
            return _parentMenuItems;
        }
        set
        {
            _parentMenuItems = value;
            NotifyPropertyChanged("ParentMenuItems");
        }
    }

    private MenuItem _selectedParentMenuItem;
    public MenuItem SelectedParentMenuItem
    {
        get
        {
            return _selectedParentMenuItem;
        }
        set
        {
            _selectedParentMenuItem = value;
            NotifyPropertyChanged("SelectedParentMenuItem");

            Mediator.NotifyColleagues(Messages.SelectedParentMenuItem, SelectedParentMenuItem);
        }
    }

    public override void MessageNotification(string message, object args)
    {
        throw new NotImplementedException();
    }
}

ChildMenuViewModel.cs

public class ChildMenuViewModel : ViewModelBase
{
    public ChildMenuViewModel()
    {
        Mediator.Register(this, new[] { Messages.SelectedParentMenuItem });
    }

    private MenuItem _currentParentMenuItem;
    public MenuItem CurrentParentMenuItem
    {
        get
        {
            return _currentParentMenuItem;
        }
        set
        {
            _currentParentMenuItem = value;
            NotifyPropertyChanged("CurrentParentMenuItem");

            ChildMenuItemsOfSelectedParent
                    = new ObservableCollection<MenuItem>(
                                                            from menuItem in XDocument.Load(Messages.DataDirectory + "MenuItems.xml")
                                                                                      .Element("MenuItems").Elements("MenuItem").Elements("MenuItem")
                                                            where (int)menuItem.Parent.Attribute("Id") == CurrentParentMenuItem.Id
                                                            select new MenuItem
                                                            {
                                                                Id = Convert.ToInt32(menuItem.Attribute("Id").Value),
                                                                Name = menuItem.Element("Name").Value,
                                                            }
                                                        );

        }
    }

    private ObservableCollection<MenuItem> _childMenuItemsOfSelectedParent;
    public ObservableCollection<MenuItem> ChildMenuItemsOfSelectedParent
    {
        get
        {
            return _childMenuItemsOfSelectedParent;
        }
        set
        {
            _childMenuItemsOfSelectedParent = value;
            NotifyPropertyChanged("ChildMenuItemsOfSelectedParent");
        }
    }

    private MenuItem _selectedChildMenuItem;
    public MenuItem SelectedChildMenuItem
    {
        get
        {
            return _selectedChildMenuItem;
        }
        set
        {
            _selectedChildMenuItem = value;
            NotifyPropertyChanged("SelectedChildMenuItem");

            Mediator.NotifyColleagues(Messages.SelectedChildMenuItem, SelectedChildMenuItem);
        }
    }

    public override void MessageNotification(string message, object args)
    {
        switch (message)
        {
            case Messages.SelectedParentMenuItem:
                CurrentParentMenuItem = (MenuItem)args;
                break;
        }
    }
}

SamplePageViewModel.cs

public class SamplePageViewModel : ViewModelBase
{
    public SamplePageViewModel()
    {
        Mediator.Register(this, new[] { Messages.SelectedChildMenuItem });
    }

    private MenuItem _currentChildMenuItem;
    public MenuItem CurrentChildMenuItem
    {
        get
        {
            return _currentChildMenuItem;
        }
        set
        {
            _currentChildMenuItem = value;
            NotifyPropertyChanged("CurrentChildMenuItem");
        }
    }

    public override void MessageNotification(string message, object args)
    {
        switch (message)
        {
            case Messages.SelectedChildMenuItem:
                CurrentChildMenuItem = (MenuItem)args;
                break;
        }
    }

Sample:

You can download the sample project that I have created here .

Problem:

Please download the sample project by clicking on the link mentioned in above line to clearly understand my problem.

  1. Run the application.
  2. As you might expect the ChildMenuView to display some items, it does not show anything initially. I think this problem occurs because ParentMenuView notifies that selectedParentMenuItem is changed before the ChildMenuView Registers itself.
  3. When you select any other ParentMenuItem, the ChildMenuView gets some data and it displays it correctly.
  4. Click on any childMenuItem, you might expect to see a page loaded and some text on the Frame. But that does not display anything. Here also I think the same problem that I mentioned in step2.
  5. Click on any other ChildMenuItem. This time Frame should display some data and then app works as expected.

So, my question is How to notify a property that Registers itself after another property has called NotifyColleagues?

Find my updated version of your App here .

<Rant> To me, the mediator pattern is just a way of not having to structure your code correctly, and I've never used it in my real code scenarios. Your demo application is a prime example where creating a collection of submodels on your ViewModel (eg ObservableCollection<ChildMenuViewModel> on ParentMenuViewModel ) makes perfect sense. By contrast, monitoring a property on the parent ViewModel from the (not even yet existing) child ViewModel seems like shooting yourself in the foot. Instead of the nice hierarchy it could be, it's a cacophony of everyone broadcasting. </Rant> .

If you really want to stay inside that pattern, you need to make sure your object is registered to the Mediator (as you've noticed in your question already) before it's supposed to catch the Mediator notification.

In the case of the Parent/ChildMenu this is easy, just rearrange MainWindow.xaml:

<Grid Grid.Row="1">
    <!-- ColumnDefinitions omitted -->
    <views:ChildMenuView Grid.Column="0" />
    <Frame Grid.Column="1" NavigationUIVisibility="Hidden" Content="{Binding SourcePage}"/>
</Grid>

<views:ParentMenuView Grid.Row="0" />

For the Frame however, it's much more complicated, because the content is instantiated dynamically (simplified: by setting the URI in the setter of the SelectedChildMenuItem). So you would need for the BindingEngine to finish updating the URI, for the Frame content to load, and only then raise your NotifyColleagues(SelectedChildMenuItem) call. This is really getting ugly... There is a workaround for everything, of course, and you can circumnavigate the worst by changing your Frame setup, binding the Content (see above) instead of the Source and instantiating the Content ( SamplePage ) before making the NotifyColleagues call:

private MenuItem _selectedChildMenuItem;
public MenuItem SelectedChildMenuItem
{
    get { return _selectedChildMenuItem; }
    set
    {
        _selectedChildMenuItem = value;
        NotifyPropertyChanged("SelectedChildMenuItem");

        LoadSourcePage(); // first instantiate the page (register it to mediator)
        Mediator.NotifyColleagues(Messages.SelectedChildMenuItem, SelectedChildMenuItem); // only now notify
    }
}

/// <summary>
/// Get the SourcePage and pass it to MainWindowViewModel
/// </summary>
private void LoadSourcePage()
{
    if (SelectedChildMenuItem != null)
    {
        var sourceUri = (from menuItem in XDocument.Load(Messages.DataDirectory + "MenuItems.xml")
                                                .Element("MenuItems").Elements("MenuItem").Elements("MenuItem")
                            where (int)menuItem.Parent.Attribute("Id") == CurrentParentMenuItem.Id &&
                                (int)menuItem.Attribute("Id") == SelectedChildMenuItem.Id
                            select menuItem.Element("SourcePage").Value).FirstOrDefault();

        var relativePart = sourceUri.Substring(sourceUri.IndexOf(",,,") + 3);

        var sourcePage = System.Windows.Application.LoadComponent(new Uri(relativePart, UriKind.Relative)); // instantiation with URI
        Mediator.NotifyColleagues(Messages.SourcePage, sourcePage); // pass on
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM