yield return Show.Busy equivalent

Jul 27, 2010 at 9:26 AM

Hi Rob,

It there any chance of implementing the Show.Busy pattern in Caliburn.Micro?

I can understand if you don't want to implement the entire ShellFramework in the super-slim Micro framework but the busy indicator was a useful start point for asynchronous processing with MEF.

Coordinator
Jul 27, 2010 at 1:38 PM

Here'a an implementation I ported for one of my current projects. But beware, you might not actually need something this complicated ;)

    public interface IBusyService
    {
        void MarkAsBusy(object sourceViewModel = null, object busyViewModel = null);
        void MarkAsNotBusy(object sourceViewModel = null);
    }


    [Export(typeof(IBusyService))]
    [PartCreationPolicy(CreationPolicy.Shared)]
    public class DefaultBusyService : IBusyService
    {
        private static readonly ILog Log = LogManager.GetLog(typeof(DefaultBusyService));
        public static string BusyIndicatorName = "busyIndicator";

        private class BusyInfo
        {
            public UIElement BusyIndicator { get; set; }
            public object BusyViewModel { get; set; }
            public int Depth { get; set; }
        }

        private readonly Dictionary<object, BusyInfo> loaders = new Dictionary<object, BusyInfo>();
        private readonly object lockObject = new object();
        private readonly object defaultKey = new object();
        private readonly IWindowManager windowManager;

        [ImportingConstructor]
        public DefaultBusyService(IWindowManager windowManager)
        {
            this.windowManager = windowManager;
        }

        public void MarkAsBusy(object sourceViewModel = null, object busyViewModel = null)
        {
            sourceViewModel = sourceViewModel ?? defaultKey;
            busyViewModel = busyViewModel ?? string.Empty;

            if (loaders.ContainsKey(sourceViewModel))
            {
                var info = loaders[sourceViewModel];
                info.BusyViewModel = busyViewModel;
                UpdateLoader(info);
            }
            else
            {
                var busyIndicator = TryFindBusyIndicator(sourceViewModel);
                if (busyIndicator == null)
                {
                    var activator = busyViewModel as IActivate;
                    if (activator == null)
                        return;

                    activator.Activated += (s,e) =>{
                        var info = new BusyInfo { BusyViewModel = busyViewModel };
                        loaders[sourceViewModel] = info;
                        UpdateLoader(info);
                    };

                    Log.Warn("No busy indicator with name '" + BusyIndicatorName + "' was found in the UI hierarchy. Using modal.");
                    windowManager.ShowDialog(busyViewModel);
                }
                else
                {
                    var info = new BusyInfo { BusyIndicator = busyIndicator, BusyViewModel = busyViewModel };
                    loaders[sourceViewModel] = info;
                    ToggleBusyIndicator(info, true);
                    UpdateLoader(info);
                }
            }
        }

        public void MarkAsNotBusy(object sourceViewModel = null)
        {
            sourceViewModel = sourceViewModel ?? defaultKey;
			if(!loaders.ContainsKey(sourceViewModel)) return;

            var info = loaders[sourceViewModel];

            lock (lockObject)
            {
                info.Depth--;

                if (info.Depth == 0)
                {
                    loaders.Remove(sourceViewModel);
                    ToggleBusyIndicator(info, false);
                }
            }
        }

        private void UpdateLoader(BusyInfo info)
        {
            lock (lockObject)
            {
                info.Depth++;
            }

            if (info.BusyViewModel == null || info.BusyIndicator == null)
                return;

            var indicatorType = info.BusyIndicator.GetType();
            var property = indicatorType.GetProperty("BusyContent");

            if (property == null)
            {
                var attribute = indicatorType.GetAttributes<ContentPropertyAttribute>(true)
                    .FirstOrDefault();

                if (attribute == null)
                    return;

                property = indicatorType.GetProperty(attribute.Name);
            }

            object content = info.BusyViewModel;

            if(!(content is string))
            {
                content = ViewLocator.LocateForModel(info.BusyViewModel, null, null);
                ViewModelBinder.Bind(info.BusyViewModel, (DependencyObject)content, null);
            }

            property.SetValue(info.BusyIndicator, content, null);
        }

        private void ToggleBusyIndicator(BusyInfo info, bool isBusy)
        {
            if (info.BusyIndicator != null)
            {
                var busyProperty = info.BusyIndicator.GetType().GetProperty("IsBusy");
                if (busyProperty != null)
                    busyProperty.SetValue(info.BusyIndicator, isBusy, null);
                else info.BusyIndicator.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
            }
            else if (!isBusy)
            {
                var close = info.BusyViewModel.GetType().GetMethod("Close", Type.EmptyTypes);
                if (close != null)
                    close.Invoke(info.BusyViewModel, null);
            }
        }

        private UIElement TryFindBusyIndicator(object viewModel)
        {
            var view = GetView(viewModel);
            if (view == null)
            {
                Log.Warn("Could not find view for {0}.", viewModel);
                return null;
            }

            UIElement busyIndicator = null;

            while (view != null && busyIndicator == null)
            {
                busyIndicator = view.FindName(BusyIndicatorName) as UIElement;

                if(busyIndicator == null)
                    view = VisualTreeHelper.GetParent(view) as FrameworkElement;
            }

            return busyIndicator;
        }

        private FrameworkElement GetView(object viewModel)
        {
            var viewAware = viewModel as IViewAware;
            if (viewAware == null)
                return null;

            return viewAware.GetView() as FrameworkElement;
        }
    }


    public class Busy : IResult
    {
        readonly object busyModel;
        readonly bool makeBusy;

        [Import]public IBusyService BusyService { get; set; }

        public Busy(bool makeBusy, object busyModel)
        {
            this.makeBusy = makeBusy;
            this.busyModel = busyModel;
        }

        public Busy(bool makeBusy)
        {
            this.makeBusy = makeBusy;
        }

        public Busy()
        {
            makeBusy = true;
        }

        public void Execute(ActionExecutionContext context)
        {
            if(makeBusy)
                BusyService.MarkAsBusy(context.Target, busyModel);
            else BusyService.MarkAsNotBusy(context.Target);

            Completed(this, new ResultCompletionEventArgs());
        }

        public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
    }

NOTE: In CM, all IResult instance are passed through IoC.BuildUp to give the container the opportunity to push dependencies in before execution. I'm taking advantage of that here. (This is going to be added to the full version of Caliburn as well.)

Jul 27, 2010 at 2:29 PM

Thanks, that's great.

I've added your code to my framework for now. Will the DefaultBusyService be included in the next release of CM? This now means that I can converted all of my current project to use CM rather than Caliburn.Full.

There is one small issue that the text of the "BusyContent" is not displayed. Should I add it to the Issue Tracker?

Coordinator
Jul 27, 2010 at 3:34 PM

This won't be added to the framework itself. However, in the near future I hope to create a CM "Recipes" section where various users can post things like this. I'm hoping that a port of the full shell framework will eventually be one of those recipes.

Jul 28, 2010 at 10:44 AM

I have found solution to the the issue with the "BusyContent" text not being displayed.

   private void UpdateLoader(BusyInfo info)
   {
      //
      //... code as before - edited to save space
      //
      if(!(content is string))
      {
         content = ViewLocator.LocateForModel(info.BusyViewModel, null, null);
         ViewModelBinder.Bind(info.BusyViewModel, (DependencyObject)content, null);
       }
       property.SetValue(info.BusyIndicator, content, null);
   }


Replace with

   private void UpdateLoader(BusyInfo info)
   {
            //
            //... code as before - edited to save space
            //
            if(!(content is string))
            {
                content = ViewLocator.LocateForModel(info.BusyViewModel, null, null);
                ViewModelBinder.Bind(info.BusyViewModel, (DependencyObject)content, null);

                property.SetValue(info.BusyIndicator, content, null);
            }
        }

The property.SetValue(...) method was clearing the existing xaml value because the "content" variable is an empty string if no BusyViewModel is specified in the Busy constructor.

Aug 4, 2010 at 9:30 AM
Edited Aug 4, 2010 at 9:48 AM

Hi, I've been playing around with Caliburn.Micro (with the SimpleContainer recipe) and tried to do something similiar to the NavigationShell. So I might have a different DLL wanting to do Show.Busy(). The problem then is that the context.Target is a LazyTaskBarItem and DefaultBusyService will not find a BusyIndicator. If I do public 

void Execute(ActionExecutionContext context) 
{ 
  var shell = IoC.Get<IShell>(); 
  if(makeBusy)
    BusyService.MarkAsBusy(shell, busyModel); 
  else 
    BusyService.MarkAsNotBusy(shell); 
  Completed(this, new ResultCompletionEventArgs()); 
}

everything works as expected. Whats the "right" way of pushing in dependencies with the SimpleContainer if it's not a IEnumerable<TService>? /Christoffer

Coordinator
Aug 4, 2010 at 1:29 PM
Edited Aug 4, 2010 at 1:30 PM

All IResult implementations go through IoC.BuildUp to allow injection of property dependencies before they are executed. If you override BuildUp on the bootstrapper and make a public IShell property it should work. Overriding BuildUp to use SimpleContainer might be a little more tricky. You would have to use reflection to get all public properties of the instance, search for properties with types that are interfaces, then resolve those interfaces individually from the container, using reflection to push them into the instance's property. Does that all make sense?

Aug 4, 2010 at 2:53 PM

Sounds a bit to tricky for me :). Maybe the shorcut is just to use a IEnumerable<IShell> in ctor's parameter and set a IShell property with .First().

 

 

Coordinator
Aug 4, 2010 at 3:07 PM

I would add this to the simple container. Give me a minute.

Coordinator
Aug 4, 2010 at 3:15 PM

Ok. The simple container recipe now had a BuildUp method. That should do the trick if you just override the method on bootstrapper to use it and add an IShell property on your IResult implementation.

Aug 4, 2010 at 5:45 PM

I've copy and pasted this into my SimpleContainer

        public void BuildUp(object instance)
        {
            var injectables = from property in instance.GetType().GetProperties()
                              where property.CanRead && property.CanWrite && property.PropertyType.IsInterface
                              select property;

            injectables.Apply(x =>
            {
                var injection = GetAllInstances(x.PropertyType);
                if (injection.Any())
                    x.SetValue(instance, injection.First(), null);
            });
        }

But it can't compile saying

Error 1 'System.Collections.Generic.IEnumerable<System.Reflection.PropertyInfo>' does not contain a definition for 'Apply' and no extension method 'Apply' accepting a first argument of type 'System.Collections.Generic.IEnumerable<System.Reflection.PropertyInfo>' could be found (are you missing a using directive or an assembly reference?) C:\Users\sechras\Documents\Visual Studio 2010\Projects\CM\samples\WMS\Framework\SimpleContainer.cs 77 25 WMS

Do you have some extension method or what?

Coordinator
Aug 4, 2010 at 6:48 PM

The extension method is in Caliburn.Micro. You may need to add a using statement to get that.

Aug 4, 2010 at 7:11 PM

BAH, shift alt F10 didn't work for that! Thx will give it a try..

Aug 4, 2010 at 7:44 PM

Added to properties

        public IBusyService BusyService { get; set; }
        public IShell Shell { get; set; }
        public void Execute(ActionExecutionContext context)
        {
            if(makeBusy)
                BusyService.MarkAsBusy(Shell, busyModel);
            else 
                BusyService.MarkAsNotBusy(Shell);

            Completed(this, new ResultCompletionEventArgs());
        }

works like a charm :). This is quite nifty to be able to do

        public static BusyResult Busy()
        {
            return new BusyResult(true, null);
        }

        public static BusyResult NotBusy()
        {
            return new BusyResult(false, null);
        }
and still get the IShell and IBusyService injected :)
but it is so easy and tempting to just use IoC.Get<T>();
Coordinator
Aug 4, 2010 at 8:58 PM

Try to avoid using the service locator directly. That's really a last resort and is mostly designed to be used by the framework internally :)

Aug 5, 2010 at 12:16 PM

What is the alternative to using the service locator?

I've been waiting for the follow on from the cited comment "In future articles I will demonstrate at least one scenario where you may be tempted to access the ServiceLocator from a ViewModel. I’ll also demonstrate some solutions.**" :-)

Coordinator
Aug 5, 2010 at 2:06 PM

I recommend having a factory method injected, which is designed to create only the type you need, rather than using the SL, which provides no indication of the class's true dependencies. Here's an excerpt from a sample I am working on for the next blog post on WP7:

using System;
using System.Collections.Generic;
using System.Linq;

[SurviveTombstone]
public class PageTwoViewModel : Conductor<IScreen>.Collection.OneActive
{
    readonly Func createTab;

    public PageTwoViewModel(Func<TabViewModel> createTab)
    {
        this.createTab = createTab;
    }

    public int NumberOfTabs { get; set; }

    protected override void OnInitialize()
    {
        Enumerable.Range(1, NumberOfTabs).Apply(x =>{
            var tab = createTab();
            tab.DisplayName = "Item " + x;
            Items.Add(tab);
        });

        ActivateItem(Items[0]);
    }
}

Notice that the constructor takes a Func<TabViewModel> rather than using the service locator. In my current work, I've been handling message boxes this way by registering a custom delegate with the IoC container (of coarse you could do this with a service as well). It looks like this:

public delegate void ShowMessageBox(string message, string title, MessageBoxOptions options = MessageBoxOptions.Ok, Action<IMessageBox> callback = null);
You can even use optional parameters with delegates :)

Aug 5, 2010 at 2:35 PM
Seems interesting. Looking forward to next post!
Coordinator
Aug 5, 2010 at 2:57 PM

Actually, I don't go into detail on that particular aspect of it, but the sample shows how it can be done. I'll try to address it more explicitly at some point int he future.

Aug 5, 2010 at 6:00 PM
Ok, but how would such handler registration look like? not sure how to assign the Func<object> in container.RegisterHandler..
Coordinator
Aug 5, 2010 at 9:11 PM

If you are using the SimpleContainer, and the Type you want to create is registered in the container, then you can request a Func<T> for that type. You do not have to register the actual factory method, the container will detect that  you are requesting one and create it for you.

Nov 11, 2010 at 9:38 PM
Edited Nov 11, 2010 at 9:40 PM

Hi Rob.

Is it possible to use the injected factory method when the type you want to create an instance of has parameters?  i.e. 

    public PageTwoViewModel(Func<string, TabViewModel> createTab)
    {
        this.createTab = createTab;
    }

  ....

  protected override void OnInitialize()

    {
        Enumerable.Range(1, NumberOfTabs).Apply(x =>{
            var tab = createTab("hello");
            tab.DisplayName = "Item " + x;
            Items.Add(tab);
        });

        ActivateItem(Items[0]);
    }


I've tried the above on the Phone and get a MissingMethodException...
Nov 11, 2010 at 10:10 PM
Edited Nov 11, 2010 at 10:15 PM

If I'm not mistaken, the SimpleContainer implementation is able to resolve constructor dependencies as long as they are registered in the container itself.

Note that the SelectEligibleConstructor function will return the construcor with the highest number of parameters, while the following function

 

    object[] DetermineConstructorArgs(Type implementation)
    {
        var args = new List<object>();
        var constructor = SelectEligibleConstructor(implementation);

        if (constructor != null)
            args.AddRange(constructor.GetParameters().Select(info => GetInstance(info.ParameterType, null)));

        return args.ToArray();
    }

 

will try to 'guess' and inject the parameter instances (which is quite nice, in my opinion... I'm thinking of using this approach in my application to build ViewModels for Models, since I don't like very much neither the injection by setting properties, nor the explicit use of the IoC container, and I have often a 1:1 or 1:n mapping between my model objects and the ViewModels).

Anyway, the MissingMethodException is quite strange... it tipically happens when loading an assembly at runtime; if such assembly was built against a certain assembly, and there is an outdated version of the reference in the AppDomain, it is fairly possible that such exception can occur... can you pin point where the exception is raised, maybe using the stack trace (I've never tested out the WP7 SDK, but I suppose you should be able to get the stack trace)?

Coordinator
Nov 11, 2010 at 11:30 PM

The factory method support in the SimpleContainer doesn't support any parameters. You would need to use a "real" IoC container for something like that. As a reasonable workaround, you can always create a custom factory and register it.

Nov 12, 2010 at 9:30 AM
Edited Nov 14, 2010 at 6:54 PM

I may have misinterpreted ksaleem question, but if PageTwoViewModel is registered in the SimpleContainer as a singleton or per-request, like

container.RegisterPerRequest(typeof(PageTwoViewModel), null, typeof(PageTwoViewModel));

or 

container.RegisterSingleton(typeof(PageTwoViewModel), null, typeof(PageTwoViewModel));

and the factory method is registered as an instance

Func<string, TabViewModel> factoryMethod = value =>
                                                       {
                                                           TabViewModel tab = new TabViewModel();
                                                           tab.DisplayName = value;
                                                           return tab;
                                                       };

container.RegisterInstance(typeof(Func<string, TabViewModel>), null, factoryMethod);

The following code

PageTwoViewModel obj = container.GetInstance(typeof(PageTwoViewModel), null) as PageTwoViewModel;

will execute properly, since the SimpleContainer can indeed resolve the constructor dependencies as I stated above.

The same happens  requesting a factory for PageTwoViewModel this way (using the same service register override)

Func<PageTwoViewModel> creator = container.GetInstance(typeof(Func<PageTwoViewModel>), null) as Func<PageTwoViewModel>;

 

So, if I'm not mistaken, the SimpleContainer implementation is able to resolve constructor injected dependencies as long as:

  1. The type having parameters in the constructor is registered as a singleton or per request (by its type)
  2. All the required parameter types are registered too

I created a sample that, in my intentions, should be a working test case.

Nov 14, 2010 at 6:47 PM

This works for me.  Thanks for your help!