How to use Caliburn Micro in a WinForms app with one WPF form

After much Googling and going through the Caliburn Micro source code, I've come up with an approach that works in a sample test application. I can't post the test application here for certain reasons, but here's the approach in a nutshell.

  • Create a WinForm with a button.
  • On button click, show a ChildWinForm
  • In the load handler of the ChildWinForm:

    
    // You'll need to reference WindowsFormsIntegration for the ElementHost class
    // ElementHost acts as the "intermediary" between WinForms and WPF once its Child
    // property is set to the WPF control. This is done in the Bootstrapper below.    
    var elementHost = new ElementHost{Dock = DockStyle.Fill};
    Controls.Add(elementHost);
    new WpfControlViewBootstrapper(elementHost);
    
  • The bootstrapper above is something you'll have to write.

  • For more information about all it needs to do, see Customizing the Bootstrapper from the Caliburn Micro documentation.
  • For the purposes of this post, make it derive from the Caliburn Bootstrapper class.
  • It should do the following in its constructor:

    
    // Since this is a WinForms app with some WPF controls, there is no Application.
    // Supplying false in the base prevents Caliburn Micro from looking
    // for the Application and hooking up to Application.Startup
    protected WinFormsBootstrapper(ElementHost elementHost) : base(false)
    {
        // container is your preferred DI container
        var rootViewModel = container.Resolve();
        // ViewLocator is a Caliburn class for mapping views to view models
        var rootView = ViewLocator.LocateForModel(rootViewModel, null, null);
        // Set elementHost child as mentioned earlier
        elementHost.Child = rootView;
    }
    
  • Last thing to note is that you'll have to set the cal:Bind.Model dependency property in the XAML of WpfControlView.

    
    cal:Bind.Model="WpfControls.ViewModels.WpfControl1ViewModel"
    
  • The value of the dependency property is used passed as a string to Bootstrapper.GetInstance(Type serviceType, string key), which must then use it to resolve the WpfControlViewModel.

  • Since the container I use (Autofac), doesn't support string-only resolution, I chose to set the property to the fully qualified name of the view model. This name can then be converted to the type, and used to resolve from the container.

Following up on the accepted answer (good one!), I'd like to show you how to implement the WinForms Bootstrapper in a ViewModel First approach, in a way that:

  1. You won't have to create a WPF Window and,
  2. You won't have to bind directly to a ViewModel from within a View.

For this we need to create our own version of WindowManager, make sure we do not call the Show method on the Window (if applicable to your case), and allow for the binding to occur.

Here is the full code:

public class WinformsCaliburnBootstrapper<TViewModel> : BootstrapperBase where TViewModel : class
{

    private UserControl rootView;

    public WinformsCaliburnBootstrapper(ElementHost host)
        : base(false)
    {
        this.rootView = new UserControl();
        rootView.Loaded += rootView_Loaded;
        host.Child = this.rootView;
        Start();
    }

    void rootView_Loaded(object sender, RoutedEventArgs e)
    {
        DisplayRootViewFor<TViewModel>();
    }

    protected override object GetInstance(Type service, string key)
    {
        if (service == typeof(IWindowManager))
        {
            service = typeof(UserControlWindowManager<TViewModel>);
            return new UserControlWindowManager<TViewModel>(rootView);
        }
        return Activator.CreateInstance(service);
    }

    private class UserControlWindowManager<TViewModel> : WindowManager where TViewModel : class
    {
        UserControl rootView;

        public UserControlWindowManager(UserControl rootView)
        {
            this.rootView = rootView;
        }

        protected override Window CreateWindow(object rootModel, bool isDialog, object context, IDictionary<string, object> settings)
        {
            if (isDialog) //allow normal behavior for dialog windows.
                return base.CreateWindow(rootModel, isDialog, context, settings);

            rootView.Content = ViewLocator.LocateForModel(rootModel, null, context);
            rootView.SetValue(View.IsGeneratedProperty, true);
            ViewModelBinder.Bind(rootModel, rootView, context);
            return null;
        }

        public override void ShowWindow(object rootModel, object context = null, IDictionary<string, object> settings = null)
        {              
            CreateWindow(rootModel, false, context, settings); //.Show(); omitted on purpose                
        }
    }
}

I hope this helps someone with the same needs. It sure saved me.