Summary of what's new

RegionManager

In the last release, the RegionManagerService was hardcoded to create only two types of Regions namely a PanelRegion which is a simple container for one view, and an ItemsControlRegion which can contain many childen. The decision on which one to use was based on whether or not the UIElement derived from a Panel or from an Item container. This works for fairly simple scenarios, but breaks down when you want to support a new type of region, say a TabRegion. It either requires you to modify the RegionManagerService to add specific support for the new type, or it requires you to create a custom control that has ends up intercepting when the item is added to the ItemsControlRegion in order to get the desired effect.

In CAB, Workspaces (which are equivalent to Regions in many ways) were extensible. All a Workspace needed was implement IWorkspace and you are done.
In Prism however because we are leveraging attached properties for registration of Regions, the case is a little different. There's only so much information we have at the time the region is registered. One option was to add an additional attached property that would allow specifying the specific type of Region to create. From a maintainbility standpoint this is a bit ugly, plus requires hard-coding in the XAML.

The solution we came up with was to have the RegionManagerService resolve the appropriate container to use, without requiring the additional attached property. The way this works is through utilizing the new

IRegion<T> and IRegionAdapter interfaces. Essentially what happens now in the RegionManager service, is when a region is added, the service looks at the UIElement type for the container element (the element that has the Region attached property set).

IRegion<T>

    public interface IRegion<T> : IRegion, IRegionAdapter
    {
       
    }

IRegionAdapter.cs

    public interface IRegionAdapter
    {
        void Initialize(DependencyObject obj);
    }

It then resolves against the container for an IRegion<T> where T is the container element type. If nothing is returned, then the service will move to the base type, and continue to walk up the hierarchy until it finds an IRegion<>. Once it does, then the Region is casted to an IRegionAdapter (IRegion<T> implements IRegionAdapter). That casted adapter will then have it's initialize method called, and the container element will be passed in. Next the adapter (which is also the region) will be added to the RegionManagerService's region dictionary which effectively registers the region.

RegionManagerService.cs

    public void SetRegion(DependencyObject containerElement, string regionName)
    {
        IPrismContainer container = PrismContainerProvider.Provider;

        Type currentType = containerElement.GetType();

        while (currentType != typeof(DependencyObject))
        {
            IRegionAdapter region = (IRegionAdapter)container.TryResolve(typeof(IRegion<>).MakeGenericType(currentType));

            if (region != null)
            {
                region.Initialize(containerElement);
                _regions.Add(regionName, (IRegion)region);
                return;
            }

            currentType = currentType.BaseType;
        }
    }

In order for the regions to be discovered, they need to be registered in the container. In the RI we are now doing this in the Bootstrapper.RegisterRegions method, though nothing stops this from happening in a module. In this case we are doing this at the bootstrapper level as these types of regions are global for all modules.

Bootstrapper.cs

    private void RegisterRegions()
    {
        container.RegisterType<IRegion<Panel>, PanelRegion>();
        container.RegisterType<IRegion<ItemsControl>, ItemsControlRegion>();
    }

So how would you now extend to add additional regions? One way would be to create a new custom control that inherits from one of the existing controls and then associate the region with it. For example you can create a new DeckPanel and simply inherit from WPF Grid without adding anything else. Then make your DeckRegion implement IRegion<DeckRegion>.

DeckPanel.cs
    public class DeckPanel : Grid
    {
        public DeckPanel()
        {
           
        }
    }

DeckRegion.cs
    public class DeckRegion : IRegion<DeckPanel>
    {
        //DeckRegion implementation goes here
        ...
    }

Next register the DeckRegion as the associated type.

    private void RegisterRegions()
    {
        ...
        container.RegisterType<IRegion<DeckPanel>, DeckRegion>();
    }

Finally go and add your region in the XAML

    <Border Grid.Row="1" BorderBrush="Navy" BorderThickness="2" >
        <!--<StackPanel Prism:RegionManager.Region="contentRegion"/>-->
        <Controls:DeckPanel Prism:RegionManager.Region="contentRegion"/>
    </Border>

Expect to see more refactoring as we go forward and more leveraging of this capability.

Collapsible region

In the previous build, the Collapsible region functionality was handled through a SlidingTemplate style that was applied to a TabControl. We weren't very happy with this as it made the XAML very difficult to maintain. We've now refactored the functionality to a CollapsibleTabControl. The control manages the collapsible state and handles various mouse events.

CollapsibleTabControl.cs

    public class CollapsibleTabControl : TabControl
    {
        public static ICommand ToggleDockedStateCommand = new RoutedCommand();
        public static DependencyProperty CollapsingStateProperty;

        static CollapsibleTabControl()
        {
	    ... 
        }

        public CollapsibleTabControl()
        {
	    ... 
        }

        void CollapsibleTabControl_ToggleDockedStateCommandExecutedRoutedEventHandler(object sender, ExecutedRoutedEventArgs e)
        {
	    ... 
        }

        void CollapsibleTabControl_MouseLeave(object sender, MouseEventArgs e)
        {
	    ... 
        }

        void CollapsibleTabControl_MouseEnter(object sender, MouseEventArgs e)
        {
	    ... 
        }

        public CollapsingState CollapsingState
        {
	    ... 
        }
    }

The control also has an associated style with a control template that is set in the a resource dictionary in the themes folder.

Generic.xaml

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:StockTraderRI.Controls">

    <Style TargetType="{x:Type local:CollapsibleTabControl}">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:CollapsibleTabControl}">

Having the functionality moved into a control, makes the Shell.xaml much cleaner and easier to maintain.

Shell.xaml

    <controls:CollapsibleTabControl x:Name="CollapsibleTab" Grid.Row="1" Grid.Column="1" prism:RegionManager.Region="CollapsibleRegion"/>

Notice the CollapsibleRegion above. You might be thinking it's using the new RegionManager functionality we described in the last section. It's not :) However, this is one of the areas I was hinting at that we might to refactor to that model (IRegion<CollapsibleTabControl> if it makes sense. Let us know if you think this makes sense.

Buy-Sell screen & Commanding infrastructure

In this iteration, our top-priority was to focus more on the commanding. In the past iterations, we looked at simple commanding scenarios. In this iteration, we wanted to focus on more complex, multi-instance scenarios. By mutli-instance, we mean multiple instances of the same screen that each have their own commands. These commands are linked to a common toolbar, which brings out the challenge of how to target the correct commands based on whichever screen is active. This is further complicated in a composite, because these screen instances may in themselves be composites, with the commands being handled by one of it's inner presenters. The other challenge is around doing a batch operation, that needs to act on all of the instances.

In order to drive this out,We added a new Buy-Sell screen which allows a trader to purchase or sell shares. The screen is based on a batching concept meaning that multiple buy-sells can be created and then be submitted either individually or in one shot. The Buy-Sell functionality contains several parts.
  • TransactionView is the composite view that represents the Buy/Sell transaction.
  • BuySellDetailsview is a child view that represents the data for the buy/sell.
  • BuySellCommandView is a child view that contains the commands for the transaction view.
  • In the shell, there is a (MainToolbarRegion* that has a TransactionControl added to it which contains buttons related to the active buy/sell transaction.
  • The PositionGrid contains a watchlistContextMenu that pops up for creating new Buy/Sell transactions.

Shell.xaml

    <ToolBarTray Grid.Row="0" Grid.ColumnSpan="2">
        <ToolBar Name="MainToolbar" prism:RegionManager.Region="MainToolbarRegion"/>
    </ToolBarTray>


TransactionControl.xaml

<Grid>
<StackPanel Orientation="Horizontal">
<Button Name="SubmitToolbarButton" Margin="2,0 2,0">Submit</Button>
<Button Name="CancelToolbarButton" Margin="0,0 2,0">Cancel</Button>
<Button Name="SubmitAllToolbarButton" Margin="0,0 2,0" Command="{x:Static inf:StockTraderRICommands.SubmitAllTransactionCommand}">Submit All</Button>
<Button Name="CancelAllToolbarButton" Margin="0,0 2,0" Command="{x:Static inf:StockTraderRICommands.CancelAllTransactionCommand}">Cancel All</Button>
</StackPanel>
</Grid>
}}
Note:We know that for this simple RI, this is somehwhat contrived. We could have easily implemented the functionality without using a Composite View. However, we've heard from customers that handling complex Composite Views that contain similar setups are fairly common. As such in order to look at adderss those scenarios, we need to address them somehow. Although in this case having a composite is probably not the most realistic, if this RI had similar to complexity as to what is common in Composite apps, it is entirely possible that you would have transactional composite views that contain commands.
Technical Challenges
The Buy/Sell screen contains quite a few techincal challenges relating to commands
  • Submit/Cancel button and toolbar buttons should only submit/cancel the current transaction.
  • Submit All/Cancel All toolbar button should submit/cancel all transactions.
  • submit/Submit All buttons and toolbar buttons should be disabled if there are any validation errors
  • Submit/Cancel button and toolbar button need to close the transaction view and remove it from the region.
All of the above relate to one thing, command routing. Now WPF has it's own built in RoutedUICommand, however as we mentioned previously, RoutedUICommands only walk up the visual tree which means the command handling must be at the view level. In this case we want the commands to route directly to the Presenters. The other problem is that RoutedUICommands, don't allow sibling views to handle the command. For example in the case of the Buy/Sell screen, the submit button lives in the BuySellCommandView, while the BuySellDetailsView which is its sibing, handles CanExecute and Execute. With a RoutedUICommand, the BuySellDetailsView will never get notified.

Also there is the issue of disabling the SubmitAll. Whenever the transaction is invalid, Submit and SubmitAll both need to disable. It is possible to handle this, but it requires a lot of wiring. What is needed is a way to wire up a set of commands together so that disabling one, disables the other and vice versa. You also need for SubmitAll, a way to have it to notify each of the different buy/sell screens that they should submit. We looked at several different ways to address this including using RoutedUICommands, and an EventBroker type mechanism. We found neither to be optimal, which led to our third option, a set of new commands including a set of core reusable commands.

TransactionCommand

The TransactionCommand is used for initiating a new Buy/Sell and is wired up to the Buy/Sell context menu in the positions screen. This command fires off a StartTransaction event passing in the appropriate parameters for whether it is a buy or a sell.

TransactionCommand.cs

    public class TransactionCommand : ICommand
    {
        public TransactionType TransactionType { get; set; }
        
        public event EventHandler CanExecuteChanged;

        public void Execute(object parameter)
        {
            var transactionInfo = new TransactionInfo() { TransactionType = this.TransactionType, TickerSymbol = parameter as string };
            StartTransaction(this, new DataEventArgs<TransactionInfo>(transactionInfo));
        }

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public event EventHandler<DataEventArgs<TransactionInfo>> StartTransaction = delegate { };
    }

DelegateCommand

This type of command does not contain logic to in the Execute or CanExecute methods, instead, it invokes delegates supplied by the class consumer. The consumer in this case is a Presenter which handles the command. Below you can see the CanExecute and Execute methods.

DelegateCommand.cs

public class DelegateCommand : ICommand
{
    Action<object> executeMethod = null;
    Func<object, bool> canExecuteMethod = null;

    public DelegateCommand(Action<object> executeMethod, Func<object, bool> canExecuteMethod)
    {
        if (executeMethod == null)
            throw new ArgumentNullException("executeMethod");

        this.executeMethod = executeMethod;
        this.canExecuteMethod = canExecuteMethod;
    }

    public bool CanExecute(object parameter)
    {
        if (canExecuteMethod != null)
        {
            return canExecuteMethod(parameter);
        }
        else
        {
            return true;
        }
    }

    public void Execute(object parameter)
    {
        executeMethod(parameter);
    }
}

MultidispatchCommand

This type of command holds a queue of commands that are to be executed when the command is executed. Users of this class can register or unregister commands. By using this class, multiple subscribers can handle a single command. Using the MultiDispatchCommand also allows each of the child commands to vote as to whether or not the command is enabled, as the CanExecute method on the MultiDispatchCommand will poll each of the children.

A common scenario for MultiDispatchCommand is when you have a SaveAll command, that executes individual Save commands. In the RI we are using the MultiDispatchCommand for the Buy/Sell scenario for SubmitAll and CancelAll. The SaveAll command automatically disables if any of the child Saves are disabled. Below you can see the Execute and CanExecute methods.

MultiDispatchCommand.cs

public class MultiDispatchCommand : ICommand
{
    //TODO: Consider making this list a weakreference in case the subscribing command needs to go away.

    readonly List<ICommand> registeredCommands = new List<ICommand>();

    public MultiDispatchCommand()
    {
    }

    public bool CanExecute(object parameter)
    {
        bool hasEnabledCommandsThatShouldBeExecuted = false;

        foreach (var command in registeredCommands)
        {
            if (ShouldExecute(command))
            {
                if (!command.CanExecute(parameter))
                {
                    return false;
                }

                hasEnabledCommandsThatShouldBeExecuted = true;
            }

        }
        return hasEnabledCommandsThatShouldBeExecuted;
    }

    public event EventHandler CanExecuteChanged = delegate { };

    public virtual void Execute(object parameter)
    {
        Queue<ICommand> commands = new Queue<ICommand>(registeredCommands);

        while (commands.Count > 0)
        {
            ICommand command = commands.Dequeue();
            if (ShouldExecute(command))
                command.Execute(parameter);
        }
    }

}

AciveAwareDelegateCommand

This class is an extension to the DelegateCommand class. This class adds the notion of "active" to the command, by implementing the IActiveAware interface, which contains the IsActive property. The IActiveAware interface is shown below:

IActiveAware.cs

public interface IActiveAware
{
    bool IsActive { get; }
    event EventHandler IsActiveChanged;
}

!!ActiveAwareMultiDispatchCommand
This class is an extension of the MultiDispatchCommand class. This class handles commands that implement the IActiveAware interface. When executing, this command only dispatches commands that are active by checking the IsActive property of each command. As each command is registered, the ActiveAwareMultiDispatchCommand subscribes to the IsActiveChanged event.
ActiveAwareMultiDispatchCommand.cs

public class ActiveAwareMultiDispatchCommand : MultiDispatchCommand
{
    protected override bool ShouldExecute(System.Windows.Input.ICommand command)
    {
        var activeAwareCommand = command as IActiveAware;

        if (activeAwareCommand == null)
        {
            return base.ShouldExecute(command);
        }
        else
        {
            return (activeAwareCommand.IsActive && base.ShouldExecute(command));
        }
    }

    public override void RegisterCommand(System.Windows.Input.ICommand command)
    {
        base.RegisterCommand(command);
        var activeAwareCommand = command as IActiveAware;
        if (activeAwareCommand != null)
        {
            activeAwareCommand.IsActiveChanged += activeAwareCommand_IsActiveChanged;
        }
    }	
}

Last edited May 15, 2008 at 3:15 PM by mconverti, version 4

Comments

No comments yet.