Wednesday, February 20, 2008

This is an intermezzo from the MVC with WF series. I have added a new sample to the project, which I hope demonstrates the flexibility of using WF.

The rest of this series can be found here:

  • Workflow as controller: Introducing <M,V,C> where M: ViewModel, V : WPF, C : WF
  • Part II, starting the application, and the adapter
  • Intermezzo: new sample application
  • Part III, your first view
  • Part IV, decoupling view from controller
  • Part V, marshalling commands from WPF to WF
  • Part VI, Injecting a controller in a subview / workspace
  • Part VII, IOC on the cheap: injecting and retrieving objects
  • Part VIII, Broadcasting for all to see

    It is a simple 4 screen 'wizard' where logic determines that it should skip one screen. Also, when the save button is hit, a popup will show that asks if you are sure. If you are, you will be sent to the first screen, otherwise you will return to your last screen. It has buttons on the left that determine where you can go, as well as 'next' and 'previous' buttons.
    All of this was done with a minimum of code and a maximum of dragging and dropping activities. The whole reason for doing this, is that when you now get a new feature request ("We have a new screen that sits in between the client and adres screen!!!"), using WF it will be dead-simple to add it.

    I have uploaded the executable here, just in case you don't feel like opening up the project and building yourself.
    The application looks like this:

    image

    And when you reach the 'Car' screen, it will look like this:

    image

    Hitting the Save button here:

    image

    A few things to notice about this sample:

    • There are 2 controllers doing their job here:
      • The usersettings controller, with a view on top. It allows you to check a checkbox. Doing so makes you an administrator. Notice how, when you do so, you are able to browse to the 'Role' screen. You see, if you are not an administrator, you are not allowed to enter the role screen.
      • The 'ImportantWizardController', which handles the mainview. It shows a few buttons (Client, Adres, Role and Car) on the left, which will allow you to go to the screens you have already passed. It also shows a previous and next screen button. Finally, it defines a contentpresenter where our subviews will be injected.
    • The buttons react immediately. Go to the Car screen, and then check your checkbox to make yourself Administrator. This means you have the right to visit the Role screen, and it immediately pops up.
    • No code behind. Nowhere. The only codebehind is on the ImportantWizardControl to 'load data' (actually returning an empty client, but you get the drift).
      It felt really cool to build this plumbing without coding.

    Let's look at the steps to produce this application:

    1. I added a Controller project (type workflow), a Domain project (with a few very simple classes), a shell project that will be used to start us up and a view project which holds the views we are going to use:
      image
    2. I created a ClientService and a UserService class which will be classes used by our workflows:
          [Serializable]
          public class UserService
          {
              public bool IsAdministrator { get; set; }

          }
          [Serializable]
          public class ClientService
          {
              public Client CurrentClient { get; set; }

          }
    3. Then the shell was used to inject our main view and also inject a global userservice class:

        1 <Window x:Class="EditLogicShell.Window1"
        2     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        3     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        4     xmlns:logic="clr-namespace:EditLogicControllers;assembly=EditLogicControllers"      
        5     xmlns:c="clr-namespace:ControllersAdapters;assembly=ControllersAdapters"
        6     Title="Window1" Height="300" Width="300">
        7     <Window.Resources>
        8         <logic:UserService x:Key="globalUserService" />
        9     </Window.Resources>
       10     <StackPanel>
       11         <Border BorderThickness="1" BorderBrush="Black" Background="Beige">
       12             <c:GenericWorkflowAdapter WorkflowController="{x:Type logic:ManageUserSettingsController}" />
       13         </Border>
       14         
       15         <c:GenericWorkflowAdapter WorkflowController="{x:Type logic:ImportantClientWizard}" />
       16     </StackPanel>
       17 </Window>

      Note line 8, where we place a UserService instance in the resources section
      On Line 12 we start our UserSettings controller
      On Line 15, we start our main wizard.

    4. Let's not go into the usersettings controller. It's just too simple. It will inject a view, and set the datacontext to the userservice class. He retrieved that class using the 'RetrieveObjectFromResources' activity.

    5. I then asked our designer (yup, that was me too... could you tell??) to design our individual views. Everywhere the designer knew he had to interact with the system, a command was created in a static class. That class turned out to be like this:

          public static class ImportantWizardInteractions
          {
              public static readonly RoutedUICommand Next;
              public static readonly RoutedUICommand Back;

              public static readonly RoutedUICommand GotoClientScreen;
              public static readonly RoutedUICommand GotoAdresScreen;
              public static readonly RoutedUICommand GotoRoleScreen;
              public static readonly RoutedUICommand GotoCarScreen;

              public static readonly RoutedUICommand Save;
              public static readonly RoutedUICommand SaveYes;
              public static readonly RoutedUICommand SaveNo;


              static ImportantWizardInteractions()
              {
                  Next = new RoutedUICommand("Next", "Next", typeof(ImportantWizardInteractions));
                  Back = new RoutedUICommand("Back", "Back", typeof(ImportantWizardInteractions));

                  GotoClientScreen = new RoutedUICommand("GotoClientScreen", "GotoClientScreen", typeof(ImportantWizardInteractions));
                  GotoAdresScreen = new RoutedUICommand("GotoAdresScreen", "GotoAdresScreen", typeof(ImportantWizardInteractions));
                  GotoRoleScreen = new RoutedUICommand("GotoRoleScreen", "GotoRoleScreen", typeof(ImportantWizardInteractions));
                  GotoCarScreen = new RoutedUICommand("GotoCarScreen", "GotoCarScreen", typeof(ImportantWizardInteractions));

                  Save = new RoutedUICommand("Save", "Save", typeof(ImportantWizardInteractions));
                  SaveYes = new RoutedUICommand("SaveYes", "SaveYes", typeof(ImportantWizardInteractions));
                  SaveNo = new RoutedUICommand("SaveNo", "SaveNo", typeof(ImportantWizardInteractions));


              }
          }
      And on individual screens, the commands were used like this:
                  <Border Background="Beige" BorderThickness="1" BorderBrush="Black" DockPanel.Dock="Left" Width="120" >
                      <ListView>
                          <Label FontWeight="Bold">Previous screens</Label>
                          <Button Command="{x:Static local:ImportantWizardInteractions.GotoClientScreen}">Client</Button>
                          <Button Command="{x:Static local:ImportantWizardInteractions.GotoAdresScreen}">Adres</Button>
                          <Button Command="{x:Static local:ImportantWizardInteractions.GotoRoleScreen}">Role</Button>
                          <Button Command="{x:Static local:ImportantWizardInteractions.GotoCarScreen}">Car</Button>
                      </ListView>
                  </Border>
      (This is the list of buttons that are shown on the left hand side of the screen)
    6. The views were passed to the developer (guess who) and the following state machine was created:
      image 

      Obviously a thing of beauty.

      1. The state initialization will retrieve the userservice, load data, set our maincontent to the mainView and set our next state to clientDetails

      2. The clientdetail has an initialization as well: it will set the datacontext to our customer and inject the ClientView as a datatemplate. Then it waits for only one command: Next. If that is triggered, it will simply move to the AdresDetails State.
        When moving out of a state, the state finalization is triggered, which will remove the view from the resources.
        Note how great it is never to have to think about that cleanup code again, it is always executed when moving out of a state.

      3. The adresDetails state has a few more commands it will listen to. When moving 'Next', a piece of logic is executed:
        image
        There is a declerative rule in the IF/ELSE that goes a little something like this: this.GlobalUserService.IsAdministrator == True
        That rule is automatically put in the rules repository and can be used by others. It determines if the next screen will be the Role screen or the CarDetails screen.

      4. Role is simple.

      5. CarDetails also reacts to the Save-command. When it get's triggered, it will inject our popup into the resources section, and move on to the save state.

      6. The Save state will react to 'SaveYes' and 'SaveNo'. It will remove the popup from the resources, and go to another state.

    7. The views are dead simple, just binding to properties. However, the ImportantWizardMainView does require our attention. It has this definition for our subview:

                  <!-- our subview, uses name: CurrentWizardScreen -->
                  <ContentPresenter 
                  Content="{Binding RelativeSource={RelativeSource FindAncestor, 
                  AncestorType={x:Type local:ImportantWizardMainView}, AncestorLevel=1}, Path=DataContext}" 
                  ContentTemplate="{DynamicResource CurrentWizardScreen}" />


      Apparently, when using the contentTemplate, the datacontext is not inherited. So I have to set it up to react to the changing datacontext of our main screen. So, when the DataContext of ImportantWizardMainView is changed, the Content of our presenter is changed to match it. (Leave comment on how I could do this simpler, if you know how).

    8. Also interesting is that I used a Grid on that view, with two children that overlay eachother. The other child is our popup screen:

              <!-- our popup lives on top of that -->
              <ContentPresenter ContentTemplate="{DynamicResource PopupScreen}" />

      When we set a datatemplate with name Popupscreen, it will be shown on top of our regular screen. I like it!

    I have added a new activity, InjectViewAsDataTemplate. We already had the InjectControllerAsDataTemplate, but there are times you don't want a whole controller.

    I've replaced the original project file with the most recent. It can be found here.
    If you are interested in seeing more about this subject, please leave a short comment!

    kick it on DotNetKicks.com

  • Wednesday, October 22, 2008 6:18:52 PM (Romance Standard Time, UTC+01:00)
    This is certainly a commendable effort. I just stumbled across this today, and found out that using WF as a controller is something that I never thought about. Please continue to share your ideas. I would be interested to know more, and to compare this with the other (more traditional) approaches.
    George
    Name
    E-mail
    Home page

    Comment (HTML not allowed)  

    Enter the code shown (prevents robots):