Frage

We are using caliburn.micro for one of our projects and I'm currently having a puzzling problem:

we have the following classes:

ToolViewerViewModel : Conductor<Screen>.Collection.OneActive
DocViewerViewModel : Conductor<DocumentViewModel>

and various document-views, all with this base class:

DocumentViewModel : Screen

The ToolViewerViewModel is to manage multiple dock-able tool views which allow the user to control different aspects of the program.

The DocViewerViewModel is to show the user the data he's working on/with. It is here to present one of the many DocumentViewModel to the user. and is implemented as a special dock-able view which can not be closed or detached from the ToolViewerView. For every aspect of the data a specific DocumentViewModel is generated by the DocViewerViewModel and presented to the user.

The DocumentViewModel is the base class for all presentation aspects of the data. One may present the data as a table an other may present it as a chart, and so on...

We now encounter problems in terms of OnActivate() and OnDeactivate() which are not called when we expect them to be called.

First Problem: The system is up and running; The DocumentViewModel is displayed in the DocViewerViewModel which is embedded in the ToolViewerViewModel along with one or two other dock-able views. The currently selected dock-able view is the DocViewerViewModel. When the user now selects one of the other dock-able views the OnDeactivate() method from the DocumentViewModel is being called. Which makes absolutely no sense to me. I'd expect the DocViewerViewModel.OnDeactivate() to be called.

Second Problem: The system is up and running; The DocumentViewModel is displayed in the DocViewerViewModel which is embedded in the ToolViewerViewModel along with one or two other dock-able views. The currently selected dock-able view is the view that enables the user to change the DocumentViewModel presented by the DocViewerViewModel. When the user now selects an other DocumentViewModel the following code is being executed within the DocViewerViewModel:

DocViewerViewModel.DeactivateItem(oldDocumentViewModel, true);
DocViewerViewModel.ActivateItem(new DocumentViewModel());

I'd expect the DocumentViewModel.OnDeactivate() to be called upon the DocViewerViewModel.DeactivateItem(oldDocumentViewModel, true) call. but that never happens.

Conclusion: The only proper working Conductor is the ToolViewerViewModel which is managing everything. But this behavior is not what we want or expect to happen: We'd like to have the ToolViewerViewModel only Conduct the dock-able views and the DocViewerViewModel to conduct the DocumentViewModel. This is important because there are two different use cases in place: One to manage multiple instances at the same time and the other where only one instance is active and used, the old instance shall be thrown away.

Hopefully anyone here can help me to get the behavior I'm looking for.

I Now have an example code for you:

public class ToolViewerViewModel : Conductor<Screen>.Collection.OneActive
{
    private readonly IDockManager _dockManager;
    private readonly DocViewerViewModel _docViewerViewModel;
    private readonly IList<DockableViewModel> _toolViews = new List<DockableViewModel>();

    public ToolViewerViewModel(IViewModelFactory viewModelFactory, DocViewerViewModel docViewerViewModel, IDockManager dockManager)
    {
        _dockManager = dockManager;
        _viewModelFactory = viewModelFactory;
        _docViewerViewModel = docViewerViewModel;
    }

    protected override void OnViewLoaded(object view)
    {
        _dockManager.Link(this);
        _dockManager.CreateSpecialPaneFor(_docViewerViewModel);
        ActivateItem(_docViewerViewModel);

        ShowToolView<ProjectExplorerViewModel>();
        base.OnViewLoaded(view);
    }

    public void ShowToolView<T>() where T : DockableViewModel
    {
        if (!IsToolViewOpen<T>())
        {
            var viewModel = _viewModelFactory.Create<T>();
            ActivateItem(viewModel);
            RefreshMenu(typeof(T));
        }
    }
}

Next class:

public class DocViewerViewModel : Conductor<DocumentViewModel>
{
    private readonly IViewModelFactory _viewModelFactory;

    public DocViewerViewModel(IViewModelFactory viewModelFactory)
    {
        _viewModelFactory = viewModelFactory;
    }

    public bool ShowInMainView<T>() where T : DocumentViewModel
    {
        return ShowInMainView(typeof(T));
    }

    private bool ShowInMainView(Type viewModelType)
    {
        var ret = false;

        // close the current view
        if (ActiveItem != null)
        {
            DeactivateItem(ActiveItem, true); //The close flag is on true since we want to remove the current instance from the memory
        }

        // check whether the current viewModel has been closed successfully
        if (ActiveItem == null)
        {
            try
            {
                var viewModel = _viewModelFactory.Create(viewModelType) as DocumentViewModel;
                if (viewModel != null)
                {
                    ActivateItem(viewModel);
                    ret = true;
                }
                else
                {
                    ActivateItem(_viewModelFactory.Create<NoDataViewModel>());
                }
            }
            catch (Exception ex)
            {
                ActivateItem(_viewModelFactory.Create<NoDataViewModel>());
            }
        }

        return ret;
    }
}

and the last one:

public abstract class DocumentViewModel : Screen
{
    private bool _isDirty;

    protected IViewModelFactory ViewModelFactory { get; private set; }
    protected IEventAggregator EventAggregator { get; private set; }

    public bool IsDirty
    {
        get
        {
            return _isDirty;
        }
        protected set
        {
            if (value.Equals(_isDirty))
            {
                return;
            }
            _isDirty = value;
            NotifyOfPropertyChange(() => IsDirty);
        }
    }

    protected DocumentViewModel(IViewModelFactory viewModelFactory, IEventAggregator eventAggregator)
    {
        ViewModelFactory = viewModelFactory;
        EventAggregator = eventAggregator;
    }

    protected override void OnDeactivate(bool close)
    {
        if (close)
        {
            if (EventAggregator != null)
            {
                EventAggregator.Unsubscribe(this);
            }
        }
        base.OnDeactivate(close);
    }

    protected override void OnActivate()
    {
        if (EventAggregator != null)
        {
            EventAggregator.Subscribe(this);
        }
        base.OnActivate();
    }

    public override void CanClose(Action<bool> callback)
    {
        var ret = true;
        if (IsDirty && (ViewModelFactory != null))
        {
            var saveDialog = ViewModelFactory.Create<SaveDialogViewModel>();
            saveDialog.Show();
            if (saveDialog.DialogResult == DialogResult.Cancel)
            {
                ret = false;
            }
            else
            {
                if (saveDialog.DialogResult == DialogResult.Yes)
                {
                    Save();
                }
                else
                {
                    Discard();
                }
                IsDirty = false;
            }
        }
        callback(ret);
    }

    public abstract void Save();

    public virtual void Discard()
    {
    }
}

With this code the only time the DocumentViewModel.OnDeactivate() is being called when the user brings an other dock-able view into focus while the DocViewerViewModel was having the focus. This should not happen!

When the user is changing the focus between the dock-able views the DocumentViewModel.OnDeactivate() should not get call. But it must get called when ever the Method DocViewerViewModel.ShowInMainView<SomeDocumentViewModel>() is being called. Which isn't the case currently.

War es hilfreich?

Lösung 4

Our Solution for that Problem is to remove the use Screen as BaseClass for DocViewerViewModel an implement the Conductor Logic our self.

Andere Tipps

As far as I can tell, there is nothing wrong with the way your code is written. Since you are using MVVM, I suggest you design a test case like I've provided here.

And here's a snippet of the test case

// TestHarness.cs

[TestMethod]
public void CheckDeactivation()
{
    // We'd like to have the ToolViewerViewModel only Conduct the dock-able views 
    // and the DocViewerViewModel to conduct the DocumentViewModel.

    IViewModelFactory factory = new ViewModelFactory();
    DocViewerViewModel docViewer = new DocViewerViewModel(factory);
    IDockManager dockManager = null;

    var toolViewer = new ToolViewerViewModel(factory, docViewer, dockManager);
    var mockToolView = new UserControl();           
    (toolViewer as IViewAware).AttachView(mockToolView);

    DocumentViewModel docView1 = new NoDataViewModel();
    DocumentViewModel docView2 = new NoDataViewModel();
    docViewer.ActivateItem(docView1);
    docViewer.ActivateItem(docView2);

    Assert.AreEqual(0, docViewer.CountDeactivated());
}

I have had the exact same problem as you, and ended up using PropertyChangedBase instead of Screen and got the problem to disappear. Later, after reading the docs on Screens and Conductors here, I realized that I wasn't activating the conductor itself further up in the view hierarchy!

So have a look at wherever you use your ToolViewerViewModel, and make sure you activate that instance!

Thank you very much for your Test. Even thought it is really nice code it tests the wrong code part. Your code simply tests whether the Method ActivateItem() or DeactivateItem() is being called:

public override void ActivateItem(DocumentViewModel item)
{
    _countActivated++;
    base.ActivateItem(item);
}

public override void DeactivateItem(DocumentViewModel item, bool close)
{
     _countDeactivated++;
     base.DeactivateItem(item, close);
}

But since these Methods are being called explicitly we don't need to test for that...

The real Problem is that the Conductor is not calling the OnActivate() or OnDeactivate() on the DocumentViewModel class. To enhance your test I used the following code:

public class DummyViewModelFactory : IViewModelFactory
{
    private readonly Dictionary<Type, Func<object>> _registredCreators = new Dictionary<Type, Func<object>>();

    public T Create<T>() where T : PropertyChangedBase
    {
        return Create(typeof(T)) as T;
    }

    public object Create(Type type)
    {
        if (type == null)
        {
            return null;
        }

        if (_registredCreators.ContainsKey(type))
        {
            return _registredCreators[type]();
        }

        return null;
    }

    public void Release(object instance)
    {
    }

    public void RegisterCreatorFor<T>(Func<T> creatorFunction)
    {
        _registredCreators.Add(typeof(T), () => creatorFunction());
    }
}

As concrete DocumentViewModel implementation I made:

public class NoDataViewModel : DocumentViewModel
{
    public NoDataViewModel(IEventAggregator eventAggregator,
        IViewModelFactory viewModelFactory)
        : base(viewModelFactory, eventAggregator, )
    {
    }

    public override void Save()
    {
        // nothing to do
    }

    public override void Reload()
    {
        // nothing to do
    }
}

public class NoDataViewModelMock : NoDataViewModel
{
    private static int activationCounterForTesting = 0;
    private static int deactivationCounterForTesting = 0;

    public static int ActivationCounterForTesting
    {
        get
        {
            return activationCounterForTesting;
        }
    }

    public static int DeactivationCounterForTesting
    {
        get
        {
            return deactivationCounterForTesting;
        }
    }

    public NoDataViewModelMock()
        : base(null, null)
    {
    }

    protected override void OnActivate()
    {
        activationCounterForTesting++;
        base.OnActivate();
    }

    protected override void OnDeactivate(bool close)
    {
        deactivationCounterForTesting++;
        base.OnDeactivate(close);
    }
}

And I changed your Testmethod to this:

[TestMethod]
public void CheckDeactivation()
{
    var viewModelFactory = new DummyViewModelFactory();

    viewModelFactory.RegisterCreatorFor<NoDataViewModel>(() => new NoDataViewModelMock());

    var docViewer = new DocViewerViewModel(viewModelFactory);
    IDockManager dockManager = null;

    var toolViewer = new ToolViewerViewModel(viewModelFactory, docViewer, dockManager);
    var mockToolView = new UserControl();
    (toolViewer as IViewAware).AttachView(mockToolView);

    docViewerViewModel.ShowInMainView<NoDataViewModel>();
    docViewerViewModel.ShowInMainView<NoDataViewModel>();
    docViewerViewModel.ShowInMainView<NoDataViewModel>();

    Assert.AreEqual(3, NoDataViewModelMock.ActivationCounterForTesting);
    Assert.AreEqual(2, NoDataViewModelMock.DeactivationCounterForTesting);
}

Then you'll see that the OnActivate() and OnDeactivate() methods are never been called.

With a little more advanced test you'd also see that they are being called but from the ToolViewerViewModel directly. I'd like to know why and how I can change this behavior to fit my needs:

  • The DocumentViewModel.OnActivate() method should get called when the DocViewerViewModel.ShowInMainView<T>() method gets called.
  • The DocumentViewModel.OnDeactivate() method should get called on the old DocumentViewModel when a new one is being shown by calling the DocViewerViewModel.ShowInMainView<T>()
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top