Showing posts with label WPF. Show all posts
Showing posts with label WPF. Show all posts

2018-06-10

Using UserControls with Caliburn.Micro

It is common to want to create a reusable UserControl, to be placed into a WPF (Windows Presentation Foundation) screen. This can be done one of two ways:
  • ViewModel First
  • View First
The techniques below will show how to do both of these schemes using Caliburn.Micro to perform the plumbing to connect them up. It took me quite a bit of research to figure out how to make these happen, particularly the View first, scheme. Both of these techniques can be used to create a UserControl in a Window that was itself generated using the other technique. For example, a window that was created using the ViewModel First scheme can include a UserControl that is created using the View First scheme.

In the example code below, the main window View is called  MainWindowView and has a ViewModel called MainWindowViewModel. The ViewModel First control has a ViewModel called ViewModelFirstTestControlViewModel, which is displayed with the View called ViewModelFirstTestControlView. The View First control has a View called ViewFirstTestControlView and has a ViewModel called ViewFirstTestControlViewModel.

The ViewModel first scheme places a ContentControl into the MainWindowView, with a x:Name attribute. For example:

<ContentControl
 x:Name="ViewModelFirstTestControlViewModel" />

The MainWindowViewModel then has this code:

namespace TestSystem.ViewModels
{
 using Caliburn.Micro;
 
 /// <summary>A ViewModel for the main window.</summary>
 /// <seealso cref="T:Caliburn.Micro.PropertyChangedBase"/>
 public class MainWindowViewModel : PropertyChangedBase
 {
  /// <summary>Initializes a new instance of the <see cref="MainWindowViewModel"/> class.</summary>
  public MainWindowViewModel()
  {
   this.ViewModelFirstTestControlViewModel = new ViewModelFirstTestControlViewModel("ViewModel First Set Content");
  }
 
  /// <summary>Gets the ViewModelFirst test control view model.</summary>
  /// <value>The ViewModelFirst test control view model.</value>
  public ViewModelFirstTestControlViewModel ViewModelFirstTestControlViewModel
  {
   get;
   private set;
  }
 }
}

So the constructor of the MainWindowViewModel instantiates the ViewModel of the UserControl, passing any arguments to initialize the values in the control. A property with the same name as the x:Name of the ContentControl exposes that ViewModel to the ContentControl. When the ContentControl needs to display the ViewModel, Caliburn.Micro finds the appropriate View and displays that as the content of the ContentControl.

The content of the actual UserControl View in this example looks like this, but could be virtually anything you want:


<UserControl
 x:Class="TestSystem.Views.ViewModelFirstTestControlView"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 <StackPanel
  <TextBlock
   Text="{Binding Path=Caption}" />
 </StackPanel>
</UserControl>

The ViewModel for the control in this example look like this:

namespace TestSystem.ViewModels
{
 using Caliburn.Micro;
 
 /// <summary>A ViewModel for the ViewModelFirst test control. This class cannot be inherited.</summary>
 /// <seealso cref="T:Caliburn.Micro.PropertyChangedBase"/>
 public sealed class ViewModelFirstTestControlViewModel : PropertyChangedBase
 {
  /// <summary>The caption.</summary>
  private string caption = "Default ViewModel first caption";
 
  /// <summary>
  /// Initializes a new instance of the <see cref="ViewModelFirstTestControlViewModel"/> class.</summary>
  public ViewModelFirstTestControlViewModel()
  {
  }
 
  /// <summary>
  /// Initializes a new instance of the <see cref="ViewModelFirstTestControlViewModel"/> class.</summary>
  /// <param name="caption">The caption.</param>
  public ViewModelFirstTestControlViewModel(string caption)
  {
   this.caption = caption;
  }
 
  /// <summary>Gets or sets the caption.</summary>
  /// <value>The caption.</value>
  public string Caption
  {
   get
   {
    return this.caption;
   }
 
   set
   {
    if (value != this.caption)
    {
     this.caption = value;
     this.NotifyOfPropertyChange(() => this.Caption);
    }
   }
  }
 }
}

The main point about the code is that there is a constructor that takes any initial values to be set for the control. You may not actually need the default constructor.

Now, let's examine how to do virtually the same thing, but do it View First. In the MainWindowView, there is this code to place the control into the View:

<ctl:ViewFirstTestControlView
 cm:Bind.Model="TestSystem.ViewModels.ViewFirstTestControlViewModel"
 Caption="View First Set Content" />

For this Xaml to work, two namespace must be defined:

 xmlns:cm="http://www.caliburnproject.org"
 xmlns:ctl="clr-namespace:TestSystem.Views" 

The cm namespace comes from the Caliburn.Micro project. Many people use "cal" instead of "cm", but I've got a namespace for "calendrics" in some of  my projects, so use cm instead. The "ctl" namespace is where your views reside.

The cm:Bind.Model specifies the ViewModel for the control. The Caption passes in the initial value of the control.

This retrieves the View for the control. The View looks very similar the the ViewModel First View, with some additions:

<UserControl
 x:Class="TestSystem.Views.ViewFirstTestControlView"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:vm="clr-namespace:TestSystem.ViewModels"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
 <UserControl.Resources>
  <vm:ViewFirstTestControlViewModel
   x:Key="ViewFirstTestControlViewModel" />
 </UserControl.Resources>
 <StackPanel
  x:Name="root"
  DataContext="{StaticResource ViewFirstTestControlViewModel}">
  <TextBlock
   Text="{Binding Path=Caption}" />
 </StackPanel>
</UserControl>


The additions specify the ViewModel for the control as a resource and binds the DataContext of the first child control to that ViewModel. However, with View First, the thing you can't avoid is having code behind. The code behind for the UserControl looks like this:

namespace TestSystem.Views
{
 using System.Windows.Controls;
 
 using TestSystem.ViewModels;
 
 /// <summary>A view first test control view.</summary>
 /// <seealso cref="T:System.Windows.Controls.UserControl"/>
 /// <seealso cref="T:System.Windows.Markup.IComponentConnector"/>
 public partial class ViewFirstTestControlView : UserControl
 {
  /// <summary>The view model.</summary>
  private ViewFirstTestControlViewModel vm;
 
  /// <summary>Initializes a new instance of the <see cref="ViewFirstTestControlView"/> class.</summary>
  public ViewFirstTestControlView()
  {
   this.InitializeComponent();
   this.vm = (ViewFirstTestControlViewModel)this.root.DataContext;
  }
 
  /// <summary>Gets or sets the caption.</summary>
  /// <value>The caption.</value>
  public string Caption
  {
   get
   {
    return this.vm.Caption;
   }
 
   set
   {
    this.vm.Caption = value;
   }
  }
 }
}

The code behind does the InitializeComponent(), then sets the ViewModel to the DataContext that was set in the view. This, in turn, is used to have the property of the control talk to the ViewModel. The ViewModel of the control looks like this:

namespace TestSystem.ViewModels
{
 using Caliburn.Micro;
 
 /// <summary>A ViewModel for the ViewFirst test control. This class cannot be inherited.</summary>
 /// <seealso cref="T:Caliburn.Micro.PropertyChangedBase"/>
 public sealed class ViewFirstTestControlViewModel : PropertyChangedBase
 {
  /// <summary>The text.</summary>
  private string caption = "Default View first caption";
 
  /// <summary>Gets or sets the text.</summary>
  /// <value>The text.</value>
  public string Caption
  {
   get
   {
    return this.caption;
   }
 
   set
   {
    if (value != this.caption)
    {
     this.caption = value;
     this.NotifyOfPropertyChange(() => this.Caption);
    }
   }
  }
 }
}

This is almost the same as the ViewModel of the ViewModel First control, except it does not need the constructors, since the property is changed from the MainWindowView. (It has a default constructor that does nothing.)

A zip file for the entire project is found here. Included are all the files, including the Caliburn.Micro bootstrapper that sets up the files.

If you know of more efficient ways of doing any of the things I've described, please let me know in the comments.

2014-02-14

Fixing WPF Default Button That Doesn't Trigger Converter

I have a textbox on a WPF form. The textbox has a converter attached to it. I also have an OK button on the form, with the IsDefault property set to true. There is a particular problem with this scenario, because the converter is normally triggered when the textbox loses the focus. However, if the user presses "Enter" rather than clicking the button, the focus is still in the textbox, so the converter never gets triggered. Since the converter hasn't been triggered, the property bound to the textbox still retains its original value, which is what the OK button commits.

Here is a little hack to get around this problem. The idea is that when Enter gets pressed, and it triggers the button click event, you set the focus to the button. This causes the LostFocus event to trigger the converter. Then you go about your business. The example below uses Caliburn.Micro, but the theory would work in other environments. The one small drawback to coding it this way is that the ViewModel has to know about the control, which makes MVVM purists queasy. I don't think this is a terrible violation of the MVVM model. It is still testable by passing null to the Ok method.

[View File]

<Button x:Name="Ok"
    cal:Message.Attach="[Event Click]=[Action Ok($source)]"
    Content="OK"
    IsDefault="True"
    MinWidth="75" />

[ViewModel File]
 
public void Ok(System.Windows.UIElement button)
{
    if (button != null)
    {
        button.Focus();
    }

    // other stuff goes here

    this.findDateDialog.TryClose(true);
}

The Click action passes the Ok button to the Ok method, which sets the focus to it. If you just click the button with the mouse, then it already has the focus, which is a no-op.

If you know a better way of doing this, let me know in the comments.

2013-01-02

Have More Control Over WPF Splash Screen Timings

I just discovered a little trick on WPF splash screens. Normally, you start a WPF splash screen like this:

public partial class App
{
    protected override void OnStartup(StartupEventArgs e)
    {
        SplashScreen splash = new SplashScreen(@"Images\splash.bmp");
        splash.Show(false, true);
        splash.Close(new System.TimeSpan(0, 0, 5));
        base.OnStartup(e);
    }
}

The problem is that the splash screen starts fading immediately. If you only give it two or three seconds, by the time the brain starts processing that it is there, it has faded to being unreadable. So to keep it readable, you need about five seconds, which is longer than I'd like it to be up.
What I'd like is to make it stay solid for 1.5 seconds, then fade out over 1.5 seconds. Here's how to do it:

public partial class App
{
    protected override void OnStartup(StartupEventArgs e)
    {
        SplashScreen splash = new SplashScreen(@"Images\splash.bmp");
        splash.Show(false, true);
        splash.Close(new System.TimeSpan(0, 0, 3));
        System.Threading.Thread.Sleep(1500);
        base.OnStartup(e);
    }
}

The Sleep causes the thread to go to sleep for 1.5 seconds. When the thread wakes up, it only has 1.5 seconds to get rid of the splash screen to meet the requirements of the TimeSpan. Perfect! It does have the drawback that the app is doing nothing on the main thread for 1.5 seconds. If your app is complex, you might need a shorter amount of sleep, or spawn another thread.