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

3

6

We have a (massive) legacy WinForms app which, through a menu item, opens up a WPF form. This WPF form will host an Infragistics grid, and some buttons/drop-downs.

This lone WPF form represents the nascent stage of a migration to WPF. Later on, more components of the app will move to WPF, and ultimately the entire app itself.

As part of the migration, we would like to use Caliburn Micro. Hence, it would be nice if we could start by using it with this lone WPF form.

  • Can someone please provide some pointers on how to use Caliburn Micro with the WPF form?
  • Or perhaps tell me why it may not make sense to use Caliburn Micro just yet?

The documentation I've read so far involves boot strappers that ensure the application starts with the desired root view model, rather than the scenario above.

Many thanks!

Eternalize answered 31/1, 2012 at 23:14 Comment(1)
I think, in this case, C.M. will cause you more trouble than solve. C.M. is meant to help pure WPF apps get "up and running" faster with it's broad / automated MVVM framework. Your application is past the stage of building from a framework, and introducing a new one at this stage will be very challenging. What will probably help you the most is applying solid MVVM (the hard way), to transition to WPF. Since the project is in WinForms, there is a non-zero chance that it was built with a decent MVC implimentation, so this transition to MVVM might be less painful. Good luck!Nuclei
E
6

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.
Eternalize answered 2/2, 2012 at 23:23 Comment(1)
You can use ViewModelBinder.Bind(rootViewModel, rootView, null); after ViewLocator.LocateForModel instead of cal:Bind.ModelDaphie
D
2

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.

Denisse answered 5/12, 2013 at 8:1 Comment(0)
C
1

Here are somethings you can start with

  • Create ViewModels and inherit them from PropertyChangedBase class provided by CM framework.
  • If required use the EventAggregator impelmentation for loosly coupled communication \ integration
  • Implement AppBootStrapper without the generic implementation which defines the root view model.

Now you can use the view first approach and bind the view to model using the Bind.Model attached property on view. I have created a sample application to describe the approach here.

Confucius answered 2/2, 2012 at 12:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.