Click here to Skip to main content
Click here to Skip to main content
Go to top

BBInterfaceNET - The first Blackberry visual designer

, 24 Jun 2012
Rate this:
Please Sign up or sign in to vote.
The article presents the architecture and the usage of a visual designer application used to create Blackberry UIs.

Sample Image

Introduction

BBInterfaceNET is a visual designer application that can be used to build Blackberry user interfaces more easily. A user can create a project in order to manage the screens and can then generate the code files that are to be imported in the Blackberry project.

I'm really happy about this article because this is my first Open Source project. You can download the app installer and the source code from the project's page on CodePlex.

This article will focus on the application architecture and usage. The application the article talks about is meant to gauge the potential usefulness of a visual designer tool for Blackberry user interfaces. Your input will be invaluable to making this project grow into a full featured designer, so please share your thoughts on the matter.

Article contents

The application architecture

BBInterfaceNET is a WPF application that uses PRISM. The application is composed of 6 modules.

  • Explorer Module - used to manage the project and project files
  • Toolbox Module - presents all the controls available to build the interfaces
  • Properties Module - used to edit the properties of the selected interface element
  • Layout Module - used to present a hierarchical view of the current screen document
  • Designer Module - used to present the documents that will be edited
  • Controls Module - this module holds the initial collection of controls used to build the BB UI

All these modules are loaded into the shell when the application starts. The module discovery is done by using the application configuration file. The listing below presents the definition for the Toolbox module.

<modules>
    <module assemblyFile="ModuleDefinitions\BBInterfaceNET.Toolbox.dll"
            moduleType="BBInterfaceNET.Toolbox.ModuleDefinition.ToolboxModule,
            BBInterfaceNET.Toolbox, Version=1.0.0.0,
            Culture=neutral, PublicKeyToken=null"
            moduleName="ToolboxModule" startupLoaded="True" >
      <dependencies>
        <dependency moduleName="BaseTypesModule"/>
      </dependencies>
    </module>
</modules>

The image below presents the main application screen as well as the containing regions.

The application was designed to be extensible but in its current version this is not possible. This extensibility will be represented by the user's possibility to add new controls that can be used to build the user interfaces. I will talk about how to do this later in the article.

The application modules

The ProjectExplorer module is used to add the functionality that is necessary to create and manage the files the application works with. The module functionality is in its single view-model, represented by the ExplorerViewModel class. This class contains the necessary code to create and delete the project files and folders. The ExplorerViewModel constructor offers some hints about how the project explorer communicates with the other parts of the application. The code can be seen in the listing below.

public ExplorerViewModel()
{
    addNewItem = new DelegateCommand<ProjectNodeBase>(OnAddNewItemCommand);
    ExplorerCommands.NewFileCommand.RegisterCommand(addNewItem);

    addExistingItem = new DelegateCommand<ProjectNodeBase>(OnAddExistingItemCommand);
    ExplorerCommands.ExistingFileCommand.RegisterCommand(addExistingItem);

    addDirectory = new DelegateCommand<ProjectNodeBase>(OnAddDirectoryCommand);
    ExplorerCommands.NewDirectoryCommand.RegisterCommand(addDirectory);

    openFile = new DelegateCommand<ProjectNodeBase>(OnOpenFile);
    ExplorerCommands.OpenFileCommand.RegisterCommand(openFile);

    deleteCmd = new DelegateCommand<ProjectNodeBase>(OnDeleteItem);
    ExplorerCommands.DeleteCommand.RegisterCommand(deleteCmd);

    renameCmd = new DelegateCommand<ProjectNodeBase>(OnRenameItem);
    ExplorerCommands.RenameCommand.RegisterCommand(renameCmd);
}

The class constructor registers various local scoped commands with some module scoped commands. These commands will be triggered when the users use the project explorer’s context menus to create and edit files. While implementing the functionality, I discovered that only defining local commands isn’t enough. Apparently the context menu can’t bind to commands in the corresponding view model. My guess is that the context menu dropdown is in a different control tree. Anyway, you can still attach the menu items in the context menu to commands if these are globally accessible. I thought that the best way to do this will be to define the commands in a static class so that they can be accessible from anywhere in the project. The commands will, of course, need to be composite commands because the logic that will be executed should not be globally exposed.

In this way we will have our local commands that can be data bound to the context menu items. In normal conditions the commands registered with a composite command will need to be deregistered. Considering the fact that there is only one project explorer instance and that this instance will expire when the application closes I felt like there was no need to deregister them. The listing below presents the definition of a single composite command.

internal static class ExplorerCommands
{
    private static CompositeCommand newFileCmd = new CompositeCommand();
    //...
    public static CompositeCommand NewFileCommand
    {
        get { return newFileCmd; }
    }
    //...
}

The code below shows this command being data bound the the context menu option.

<MenuItem Header="Add New Item" Command="{x:Static cmd:ExplorerCommands.NewFileCommand}" CommandParameter="{Binding}" ></MenuItem>

The command parameter in the above code represents the current element in the tree that was clicked.

The other way in which the ExplorerViewModel class communicates is through normal events. Events are triggered in the following situations: when an item is created, added (existing item), opened, deleted or renamed and when the project is closed. I chose this implementation because I thought the functionality could be reused in projects that do not use PRISM. The other option would have been to publish CompositePresentationEvents.

Since there will be a need to access the explorer functionality from other modules the explorer class implements the IProjectExplorer interface. This interface definition can be seen below.

public interface IProjectExplorer
{
    //opens an existing project
    ProjectInfo OpenProject(string projectFilePath);
    //closes a project
    void CloseProject();
    //creates a brand new project
    void CreateNewProject(ProjectInfo projectInfo);
    //is the project opened?
    bool IsOpened();
    //project path and name
    string ProjectName { get; }
    string ProjectPath { get; }

    event EventHandler<FileEventArgs> ItemCreated;
    event EventHandler<FileEventArgs> ItemAdded;
    event EventHandler<FileEventArgs> ItemOpened;
    event EventHandler<FileEventArgs> ItemDeleted;
    event EventHandler<FileRenamedEventArgs> ItemRenamed;
    event EventHandler<FileEventArgs> ProjectClosed;
}

As you can see, the interface exposes a few basic methods and all the events I previously discussed. The last thing to discuss regarding this module is how the view is registered and how the module communicates with the other parts of the application. This is all done in the module definition class. This class can be seen below.

public class ProjectExplorerModule:IModule
{
    IUnityContainer container;
    IEventAggregator eventAggregator;

    public ProjectExplorerModule(IUnityContainer container, IEventAggregator eventAggregator)
    {
        this.eventAggregator = eventAggregator;
        this.container = container;
    }
    public void Initialize()
    {
        container.RegisterInstance<IProjectExplorer>(new ExplorerViewModel());
        IRegionManager regionManager=container.Resolve<IRegionManager>();
        regionManager.RegisterViewWithRegion("ExplorerRegion", typeof(ExplorerView));
        SubscribeToExplorerEvents();
    }
    private void SubscribeToExplorerEvents()
    {
        IProjectExplorer explorer = container.Resolve<IProjectExplorer>();
        explorer.ItemCreated += (s, args) =>
        {
            eventAggregator.GetEvent<FileCreatedEvent>().Publish(args.FilePath);
        };
        //...
    }
}

The Initialize method first registers an instance of the explorer because this will need to be accessed from the shell when projects are created, opened or closed. The method than registers the view with the explorer region. When the region will be displayed, a view instance will be created and the registered IProjectExplorer instance will be injected in the view’s constructor like in the code below.

public ExplorerView(IProjectExplorer viewModel)
{
    InitializeComponent();
    this.Loaded += (s, e) => { DataContext = viewModel; };
}

After the view is registered, the module subscribes to the normal events and triggers composite presentation events that will be used to notify other parts of the application.

The Toolbox module is used to display all the controls that are loaded into the application and that can be used to build the Blackberry interfaces. The module definition file can be seen below.

public class ToolboxModule:IModule
{
    IUnityContainer container;
    public ToolboxModule(IUnityContainer container)
    {
        this.container = container;
    }
    public void Initialize()
    {
        container.RegisterInstance<IToolboxService>(new ToolboxService(container));
        IRegionManager regionManager = container.Resolve<IRegionManager>();
        regionManager.RegisterViewWithRegion("ToolboxRegion", typeof(ToolboxView));
    }
}

The module first registers a local implementation of the IToolboxService. This implementation retrieves all the relevant controls that are registered with the unity container. Every module that supplies new components will need to register those components with the UnityContainer. This will make all the new components available for use. After this the view is registered with its corresponding region. The view model is injected into the view's constructor when the view is created. All the view-model does is to expose the collection of controls it gets from the service. This can be seen below.

public List<ToolboxItem> Items
{
    get
    {
        if (items == null)
        {
            items = toolboxService.GetItems().OrderBy(p => p.Description).ToList();
            CollectionView cv = CollectionViewSource.GetDefaultView(items) as CollectionView;
            if (cv != null)
                cv.GroupDescriptions.Add(new PropertyGroupDescription("Category"));
        }
        return items;
    }
}

The PropertiesWindow module is used to edit the selected control properties. When a control is selected in the designer, a composite presentation event is raised with the corresponding control. The PropertiesViewModel registers for this event and executes the required logic. This can be seen in the code below.

public PropertiesViewModel(IEventAggregator eventAggregator)
{
    this.eventAggregator = eventAggregator;
    FieldSelectedEvent evt = eventAggregator.GetEvent<FieldSelectedEvent>();
    evt.Subscribe(OnSelectedFieldChanged, ThreadOption.UIThread);
}
public void OnSelectedFieldChanged(Field selField)
{
    //call ToList() to make a new list with the same elements
    SelectedField = selField;
}

The view-model also publishes an event when the value changes. This is in order to mark the current document as dirty. The code that does this can be seen below.

public void RaiseFieldPropertyChanged(object newValue, object oldValue)
{
    FieldChangedEvent evt = eventAggregator.GetEvent<FieldChangedEvent>();
    evt.Publish(new Events.Model.FieldChangedData() { NewValue = newValue, OldValue = oldValue });
}

This method is triggered from a behavior when the PropertyGrid’s PropertyValueChanged event is fired. The Module definition can be seen below.

public void Initialize()
{
    regionManager.RegisterViewWithRegion("PropertiesRegion", typeof(PropertiesView));
}

The view-model is injected into the view’s constructor when this is created.

The LayoutWindow module is used to show a hierarchical view of the current document. There will be some instances in which the user will not be able to edit the current document by just using the designer window. In these cases the user can use the layout window for more control. This layout window is also the only way the user can add a title, banner and a status to the screens. Seeing that this is just another view for the designer, there are no view-models in this module. There is only the view. The code below presents the code that registers the view with the region. This code uses the second overload of the RegisterViewWithRegion method (not exactly sure why).

public void Initialize()
{
    LayoutView view=new LayoutView() { DataContext = null };
    IRegionManager regionManager = container.Resolve<IRegionManager>();
    regionManager.RegisterViewWithRegion("LayoutRegion", () => { return view; });
}

The last module is the Designer module. This module presents the application designer, the view that will display the list of active documents. The module initialization method can be seen below.

public void Initialize()
{
    container.RegisterInstance<IAddFieldService>(new AddFieldService());
    IRegionManager regionManager=container.Resolve<IRegionManager>();
    regionManager.RegisterViewWithRegion("DesignerRegion", typeof(DesignerView));
}

The module first registers a service. This service is an UI interaction service that is used to present the AddFieldDialog window. The user can use this window to add new fields to the screen without using the toolbox drag and drop operation. After this, the Designer view is registered with the corresponding region. The more I think about it the more I feel I should have used the InteractionRequest pattern instead of the UI service. I used the service because PRISM doesn’t support the InteractionRequest pattern for WPF. In the meantime I replicated the Silverlight functionality. This will be replaced in a future version.

The designer view-model will be injected into the view when this is created. The designer view-model registers for all the composite presentation events that are published by the explorer module. This can be seen below.

public DesignerViewModel(IEventAggregator eventAggregator, IUnityContainer container,
    IDesignerPersistenceService persistenceService)
{
    this.eventAggregator = eventAggregator;
    this.container = container;
    files = new ObservableCollection<DocumentViewModel>();
    this.persistenceService = persistenceService;

    CloseProjectEvent clProjEvt = eventAggregator.GetEvent<CloseProjectEvent>();
    clProjEvt.Subscribe(OnProjectClosed, ThreadOption.UIThread);
    FileOpenedEvent openFilesEvt = eventAggregator.GetEvent<FileOpenedEvent>();
    openFilesEvt.Subscribe(OnFileOpened, ThreadOption.UIThread);
    FileDeletedEvent delFilesEvt = eventAggregator.GetEvent<FileDeletedEvent>();
    delFilesEvt.Subscribe(OnFileDeleted, ThreadOption.UIThread);
    FileRenamedEvent renFileEvt = eventAggregator.GetEvent<FileRenamedEvent>();
    renFileEvt.Subscribe(OnFileRenamed, ThreadOption.UIThread);
    FileCreatedEvent createdEvt = eventAggregator.GetEvent<FileCreatedEvent>();
    createdEvt.Subscribe(OnFileCreated, ThreadOption.UIThread);

    this.PropertyChanged += (s, a) =>
    {
        if (a.PropertyName == "SelectedFile")
        {
            IRegionManager rm = container.Resolve<IRegionManager>();
            IRegion reg = rm.Regions["LayoutRegion"];
            if (reg == null) return;
            IView view = reg.Views.FirstOrDefault() as IView;
            if (view != null)
            {
                view.DataContext = SelectedFile;
            }
        }
    };
}

The constructor also creates the list of documents and listens for the PropertyChanged event on one of its own properties. When the selected document is changed the LayoutView data context is changed. I don’t really like this code, but at this time I can’t see any other solution to update the LayoutView.

In the designer view I used a couple of automatic data templates in order to display the 2 supported designer views: the screen view and the image view. The xaml can be seen below.

<DataTemplate DataType="{x:Type vm:FileDesignerViewModel}">
    <v:FileDesignerView />
</DataTemplate>

<DataTemplate DataType="{x:Type vm:ImageDesignerViewModel}">
    <v:ImageDesignerView />
</DataTemplate>
<TabControl ItemsSource="{Binding Path=Files}" BorderThickness="0"
            SelectedItem="{Binding Path=SelectedFile, Mode=TwoWay}"
            ItemTemplate="{StaticResource ClosableTabItemTemplate}"
            Background="Transparent" Padding="0"
            >
    <TabControl.ItemContainerStyle>
        <Style TargetType="{x:Type TabItem}">
            <Setter Property="Background" Value="WhiteSmoke"/>
        </Style>
    </TabControl.ItemContainerStyle>
</TabControl>

This will allow the designer code to add new elements to the document collection and these will be automatically displayed because of the data templates. I think a more elegant solution would have been to have another region here and then use the new PRISM navigation feature. At the time I developed the designer I still didn’t have a grasp on the navigation but I will certainly change this part to use navigation as this is a perfect candidate.

When a file opened event is triggered the designer will create a new view-model and will add that to the document collection.

The rest of the modules are modules that contain the controls used to build the Blackberry UIs. At the moment the application contains only one such module. Users can add others if they wish to extend the application. There is some interesting code in this module that I will like to talk about. The module initialization method can be seen below.

public void Initialize()
{
    //merge the resource dictionary
    IDictionaryMergingService mergingService = container.Resolve<IDictionaryMergingService>();
    mergingService.MergeDictionary(new BaseFieldsRS());

    var types = Assembly.GetExecutingAssembly().GetTypes().Where(
                p => !p.IsAbstract && p.IsSubclassOf(typeof(Field))).ToList();
    foreach (var type in types)
    {
        container.RegisterType(typeof(Field), type, type.AssemblyQualifiedName);
    }            
}

Besides registering the new controls that the module wants to add, the module does something else. It uses the IDictionaryMergingService to merge the controls’ data templates into the shell application’s resource dictionary. This is what makes it possible to show controls added by new modules without the original app knowing about how they should look. The IDictionaryMergingService is registered in the shell. The control data templates are applied automatically by type and they are applied recursively when there is a manager that needs to display its children.

Regions and view Registration

Regions act as placeholders for one or more views that will be displayed at run time. Modules can locate and add content to regions in the layout without knowing how and where the regions are displayed. This allows the layout to change without affecting the modules that add the content to the layout.

Regions are sometimes used to define locations for multiple views that are logically related. In this scenario, the region control is typically an ItemsControl-derived control that will display the views according to the layout strategy that it implements, such as in a stacked or tabbed layout arrangement.

Regions can also be used to define a location for a single view; for example, by using a ContentControl. In this scenario, the region control displays only one view at a time, even if more than one view is mapped to that region location.

The BBInterfaceNET shell contains 5 regions. In PRISM regions can be defined in xaml or in code. The application uses the first option. The xaml code below shows a stripped down version of the region declarations.

<ContentControl regions:RegionManager.RegionName="ExplorerRegion" />
<ContentControl regions:RegionManager.RegionName="LayoutRegion" Grid.Row="2" />
<ContentControl Grid.Column="2" regions:RegionManager.RegionName="DesignerRegion" />
<ContentControl  regions:RegionManager.RegionName="ToolboxRegion" />
<ContentControl Grid.Row="2" regions:RegionManager.RegionName="PropertiesRegion"/>

As soon as the RegionName attached property is set on a ContentControl or on an ItemsControl a region is created in the default RegionManager. When the application starts and the modules are loaded, these modules register views with these regions.

In PRISM there are 2 ways to register a view with a region: view discovery and view injection. View discovery is used to register views that don’t change very often. View Injection is used to add views dynamically and is appropriate when the views in a region change often. The application uses view discovery to register views with the 5 regions in the shell. By using view discovery, when a particular region is shown, the views registered with that region are automatically loaded. Each module registers a corresponding view when it is initialized, guaranteeing that when the application’s main window is shown so are the 5 views. The code below presents the registration for the designer view.

public void Initialize()
{
   container.RegisterInstance<IAddFieldService>(new AddFieldService());
   IRegionManager regionManager=container.Resolve<IRegionManager>();
   regionManager.RegisterViewWithRegion("DesignerRegion", typeof(DesignerView));
}

As can be seen from the above code, the current implementation registers a single view with the designer view by using view discovery. I kept thinking about this. In the designer the number of views changes frequently as the user opens and closes the documents.

A better implementation would be to use view injection or even navigation. I think view injection would work best here instead of view discovery or navigation though. Navigation should be used when there are multiple views to navigate and only one can be shown at a time. Also navigation should be used if the user might need to validate or save the current step before moving to the next. A next version of the application will use view injection.

Using commands to communicate between modules

Inter-module communication can be done in a number of ways in PRISM. We can communicate by using commands, the region context, event aggregators and shared services. The application presents a few scenarios where communicating by using commands is the best choice. These situations are: saving files, closing files and closing the application.

Saving files

The commands used to save the files are triggered from the shell. The code that actually saves the files is implemented in separate module (the DesignerModule). In order to link the two we will need to use some sort of global commands. Using a DelegateCommand in this case and making it globally accessible is not the right choice because the user may want to save all the files at once. Luckily PRISM offers the CompositeCommand class. The CompositeCommand class is a collection of commands that are executed in order. When a CompositeCommand is triggered all the registered commands are executed in sequence. Using CompositeCommands is the best way to implement the save functionality for the application.

The application has two save commands: save and save all. Save all will save all the opened documents while the save command will only save the active document. There is an interesting thing to metion here about the save functionality. The save command will be inactive for a document that is not dirty. This presents a problem with the save all command. By default a CompositeCommand can execute only if all registered commands can be executed. This means that by default if a document in the list of active documents is left unchanged the other documents can't be saved by using the save all command.

In order to change this i overrode the default CompositeCommand implementation. In particular I derived from the CompositeCommand class and changed the CanExecute implementation to allow the CompositeCommand to be triggered if at least one of the registered commands could be executed. The code can be seen below.

public class CustomCompositeCommand:CompositeCommand
{
	public CustomCompositeCommand():base()
	{}
	public CustomCompositeCommand(bool monitorCommandActivity)
		: base(monitorCommandActivity)
	{}
	public override bool CanExecute(object parameter)
	{
		ICommand[] commandList;
		bool hasEnabledCommandsThatShouldBeExecuted = false;
		lock (this.RegisteredCommands)
		{
			commandList = this.RegisteredCommands.ToArray();
		}
		foreach (ICommand command in commandList)
		{
			if (this.ShouldExecute(command))
			{
				if (command.CanExecute(parameter))
				{
					hasEnabledCommandsThatShouldBeExecuted = true;
				}
			}
		}
		return hasEnabledCommandsThatShouldBeExecuted;
	}
}

The interesting part of the code happens in the for loop. The loop iterates over all the registered commands and if at least one of then can be executed the CanExecute method for the CompositeCommand returns true. This is somewhat different from the original implementation in that the method returned true only if all commands could have been executed.

My implementation presents a risk. If the user does nothing else command that shouldn't execut may execute. In order to fix this every view-model that registers commands with this type of CompositeCommand sould add a check in the execute handler and execute the method only if the current command can execute. This can be seen below.

private void OnSaveCommand()
{
	if (CanSaveOverride)
		SaveOverride();
}

In order to make the CompositeCommands available between modules they were defined as static members in a publicly accessible class.

public static class InfrastructureCommands
{
	private static CompositeCommand saveAllCmd, saveCmd, shutdownCmd;

	static InfrastructureCommands()
	{
		saveAllCmd = new CustomCompositeCommand();
		saveCmd = new CustomCompositeCommand(true);
		shutdownCmd = new CompositeCommand();
	}

	public static CompositeCommand SaveAllCommand
	{
		get { return saveAllCmd; }
	}
	public static CompositeCommand SaveCommand
	{
		get { return saveCmd; }
	}
	public static CompositeCommand ShutdownCommand
	{
		get { return shutdownCmd; }
	}
}

View-models can then register their own commands with these. The code below presents how the save and save all commands for the documents were registered.

saveCmd = new DelegateCommand(OnSaveCommand, () => CanSaveOverride);
InfrastructureCommands.SaveAllCommand.RegisterCommand(saveCmd);
InfrastructureCommands.SaveCommand.RegisterCommand(saveCmd);

You can see from the code above that we register the same command with both the save and save all composite commands. These commands differ in one way. The save CompositeCommand sets the monitorCommnadActivity constructor parameter to true. This means that this CompositeCommand will only execute the registered commands that are active and that can be executed. This can be determined because the CompositeCommand class implements the IActiveAware interface.

When the monitorCommandActivity parameter is true, the CompositeCommand class exhibits the following behavior:

  • CanExecute. Returns true only when all active commands can be executed. Child commands that are inactive will not be considered at all.
  • Execute. Executes all active commands. Child commands that are inactive will not be considered at all.

To support this the view-models should implement the IActiveAware interface. The interface is primarily used to track the active state of a child view in a region. Whether or not a view is active is determined by the region adapter that coordinates the views in the specific region control. For example, the Tab control uses a region adapter that sets the view in the currently selected tab to active. When the view-model's IsActive property changes you can change the corresponding commands' IsActive property.

The way the designer is currently implemented doesn't seem to work very well with the automatic setting of the IsActive property by the region manager. Apparently the region manager doesn't set the IsActive property if you work with data templates (the current implementation does this). In order to fix this i set the IsActive property manually when the current document changes. The code for this can be seen in the DesignerViewModel.

public DocumentViewModel SelectedFile
{
	get { return selFile; }
	set
	{
		if (selFile != value)
		{
			if (selFile != null) selFile.IsActive = false;
			selFile = value;
			if (selFile != null) selFile.IsActive = true;
			RaisePropertyChanged(() => SelectedFile);
		}
	}
}

Now when the current document changes i also set the IsActive property for all the commands in that view-model. This can be seen below.

protected override void OnIsActiveChanged()
{
	base.OnIsActiveChanged();

	SaveCommand.IsActive = IsActive;
	//publish the selected field
	if (IsActive)
	{
		if (Screen != null)
			eventAggregator.GetEvent<DocumentChangedEvent>().Publish(Screen as MainScreen);

		eventAggregator.GetEvent<FieldSelectedEvent>().Publish(selectedField);                
	}
}

Application shutdown

One of the problems i faced when designing the application was to decide what would be the best way to handle the application shutdown. For aplications that don't need to save documents this is easy. All you need to do is trigger an event when the user triggers the close command from the menu. Then you would call the window Close method.

When you have an application that works with documents the shutdown operation is a little harder. You need to account for all the ways the application can be closed. If you have unsaved data you also need to ask the user how he/she wants to handle the unsaved documents.

For the current application all these problems need to be solved. The application can close if the user triggers the Exit command or if he presses the x button on the main window. To handle the Exit command case, the ShellViewModel class exposes the Exit command and the Shutdown event. If the command is triggered, so is the event. This can be seen below.

private void OnShutDown()
{
	if (Shutdown != null)
		Shutdown(this, EventArgs.Empty);
}

Than in the Bootstrapper's InitializeShell method i subscribe to the event and call the Close method on the view.

Shell shell = (Shell)this.Shell;
ShellViewModel vm = Container.Resolve<ShellViewModel>();
shell.DataContext = vm;
vm.Shutdown += (s, e) => {
	shell.Close();
};

The problem now is to handle the unsaved documents. You have to consider here that the documents are implemented in another module and that the shell doesn't have access to that code. To gather the required information i used a global composite command. In the main view's Closing event handler this command is executed.

shell.Closing += (s, e) => {
	if (InfrastructureCommands.ShutdownCommand.CanExecute(e))
		InfrastructureCommands.ShutdownCommand.Execute(e);
	//...
}

As you can see, the command CanExexute and Execute mothods are passed the CancelEventArgs instance of the main view's Closing event. All the opened documents will subscribe to this command and will modify the Cancel property if they need to be saved. This can be seen below.

shutdownCmd = new DelegateCommand<CancelEventArgs>(ShutdownOverride);
InfrastructureCommands.ShutdownCommand.RegisterCommand(shutdownCmd);
protected override void ShutdownOverride(CancelEventArgs args)
{
	if (IsDirty) args.Cancel = true;
}

In the bootstrapper we than check the Cancel property value. The rest of the Closing event handler can be seen below.

if (e.Cancel)
{
	//display the dialog to ask for directions
	IInteractionService intService = Container.Resolve<IInteractionService>();
	intService.ShowConfirmationDialog("Shutdown", "There are still some unsaved documents. Do you want to save them before closing?",
		(res) => {
			if (res!=null && res.Value)
			{//save and exit
				if (InfrastructureCommands.SaveAllCommand.CanExecute(null))
					InfrastructureCommands.SaveAllCommand.Execute(null);
				e.Cancel = false;
			}
			else if (res!=null && !res.Value)
			{//don't save and exit
				e.Cancel = false;
			}
		});
}

If the shutdown operation was cancelled it means we have unsaved documents. In this case we present the user with a dialog that asks them how to proceed. The user can now save the changes before closing, ignore the changes or cancel the shutdown. If the user decides to save, the Save All command is triggered. This, in turn, will trigger the Save command in each open document. The Cancel property will than be set to false in order to allow the application to close.

Closing documents

To close a document the user will press the X button for the corresponding tab item. This button is bound to the CloseCommand of the base DocumentViewModel class. All documents will derive from this class. The close command is a regular DelegateCommand that can be executed at anytime. The implementation for the execute handler can be seen below.

private void OnClose()
{
	CancelEventArgs args = new CancelEventArgs();
	CloseOverride(args);

	if (!args.Cancel)
	{
		InfrastructureCommands.SaveAllCommand.UnregisterCommand(SaveCommand);
		InfrastructureCommands.SaveCommand.UnregisterCommand(SaveCommand);
		InfrastructureCommands.ShutdownCommand.UnregisterCommand(ShutdownCommand);
		FileClosed(this, EventArgs.Empty);
	}
}

The method first creates a CancelEventArgs instance and passes it to the CloseOverride method. This method is a virtual method that can be overridden in the derived classes. In the derived classes, documents that can't be closed at that moment will cancel the close operation by setting the argument Cancel property to true. After the method returns this property is analyzed. If the Cancel property is false the view-model will unregister the save and shutdown commands and trigger the FileClosed event.

In the existing derived DocumentViewModels only the FileDesignerViewModel has the option of cancelling the file close operation. This is because only this type of document can be modified. The CloseOverride implementation in this view-model looks like the code below.

protected override void CloseOverride(CancelEventArgs args)
{
	if (IsDirty)
	{
		IConfirmationService service = container.Resolve<IConfirmationService>();
		service.ShowConfirmationDialog("Close File", 
		"The file has been modified. Do you want to save before closing?",
			res => {
				if (res == null)
					args.Cancel = true;
				else if (res != null && res.Value)
				{//save
					SaveOverride();
				}
				else{/*don't save*/}
			});
	}
}

You can see that we run code only if the document is dirty. If the document has been modified and the user chooses to close, an interaction service presents a confirmation dialog. Based on the response, the document is saved before closing, the changes are discarded or the close operation in cancelled.

The current implementation uses a custom interaction service. Another alternative would be to use an InteractionRequest pattern. In this situation we will need to use a custom window type. This is because the default confirmation window only has 2 buttons. For this interaction we will need 3 (yes, no, and cancel).

Using the application

The application has two primary usage scenarios: creating and editing UI screens and generating Java code.

Creating and editing UI screens

In order to start using the application we need to create a project. This project will be used to manage the screen files. The image below presents the New Project dialog.

As can be seen from the previous image, you can use this window to specify the project storage path, the BB operating system version and the BB device model. These last two settings are necessary in order to set the screen size and the default font size when editing the documents.

Once we click ok, the project will be created and the project file can be seen in the explorer window. Now the user will have the possibility to add files to the project in order to create the BB screens. This can be done by right clicking the project name in the explorer and choosing the Add New Item option.

As soon as a file is created, that file will be visible in the explorer window and it will also be automatically opened in the main region of the application (in the designer). This can be seen in the image below.

The designer surface size is dependent on the device model. This is why we needed to specify it when we created the project. After the file is created the user can start adding controls to the designer. This can be done either by dragging and dropping the controls from the toolbox window or by using the layout window. The image below presents a screen after a few elements have been dropped from the toolbox.

The other option we have for adding controls to the screen is to use the Layout Window. In this window we can add and remove controls to and from any manager. The window can also be used to set the title, banner and status for the screen. In fact, this is the only way, at the moment, in which you can change these screen properties.

To add elements to a manager you use the plus icon. Pressing this opens up the Add New Field dialog. If we click ok the field is added either as a sibling or as a child of the currently selected control, depending on the control type (non manager or manager respectively). The image below presents this window.

In order to change the selected element's properties we can use the properties window. The image below presents how to edit the background color of a label field.

Saving changes and generating code

Once the screens have been designed we can save them in order to start the code generation. In order to generate the code we use the Generate menu option of the Project menu. The files are saved in XML format. Not only that but the XML has a very simple structure. This is in order to support a future version that will allow the user to add controls to the screen by writing XML. The image below presents the structure of such a file.

The application will generate the corresponding java code for our project files. The generated code is MVC code. The application will use the file names as the names of the view classes (the classes that derive from the MainScreen class). If the file names don't end if "View" the app will automatically suffix the file names. The app also generate one controller class for each view class. This can be seen in the image below. The image presents the list of project files, the generated views and the generated controllers.

The listing below presents the code generated for one of the designed views. We can see from this listing that the view has a reference to the corresponding controller. This will help us delegate the tasks when user events are triggered.

//the class definition
public class HomeView extends MainScreen{

    //Constructors
    public HomeView(HomeViewController controller){
        super();
        this.controller=controller;
        initComponents();
    }
    public HomeView(HomeViewController controller, long style){
        super(style);
        this.controller=controller;
        initComponents();
    }
    //Field initialization
    private void initComponents(){
        labelField1 = new LabelField();
        labelField1.setText("Click to go the the settings page");
        labelField1.setBackground(BackgroundFactory
            .createSolidTransparentBackground(0x00C8C800, 200));
        this.add(labelField1);

        buttonField1 = new ButtonField(Field.FIELD_RIGHT);
        buttonField1.setLabel("Start");
        this.add(buttonField1);
    }
    //Fields
    public LabelField labelField1;
    public ButtonField buttonField1;
    private HomeViewController controller;
}

The listing below presents the code for the corresponding controller.

public class HomeViewController {

    private MainScreen view;
    
    public HomeViewController(){
        
    }
    public MainScreen getView(){
        if(view==null)
            view=new HomeView(this);
        return view;
    }
    public void showView(){
        UiApplication.getUiApplication().pushScreen(getView());
    }
}

Integrating the generated files into a Blackberry project

All that is necessary in order to build our Blackberry app at this point is to copy the files to the Blackberry project and to import them by using the JDE. This job is even easier considering the fact that the application generate the correct folder structure. The image below shows a Blackberry project.

The code listing below presents the application class code that is used to start the app.

public class App extends UiApplication {
    public App() {
        HomeViewController ctrl=new HomeViewController();
        ctrl.showView();
    }
    public static void main(String[] args) {
        App app=new App();
        app.enterEventDispatcher();
    }
}

The code below uses one of the generated controller classes in order to show the first application screen. Next we need to add some navigation code in order to change the screen when the user presses a button. This is a very easy task because of the MVC architecture. To move to the next screen we will add a button handler in the HomeView view class and in this handler we will delegate to the controller class. This will be done in the initComponents method.

//...
buttonField1.setChangeListener(new FieldChangeListener() {
    public void fieldChanged(Field arg0, int arg1) {
        controller.moveToNextPage();
    }
});

The code for the moveToNextPage method can be seen in the listing below.

//...
public void moveToNextPage(){
    SettingsViewController ctrl= new SettingsViewController();
    ctrl.showView();
}

Running the Blackberry application

The image below presents the two screens as they appear in the Blackberry emulator.

The image below presents the designed screens as they appear in the BBInterfaceNET application.

The UIs are a bit different but this will be solved by adjusting the control styles.

Known issues

This application is far from finished. I decided to make it public in order to see if there is a real need in the industry for a Blackberry visual designer. I always wandered why there isn't a Blackberry designer available even though every other modern mobile technology has one (WP7, Android and iOS all have visual designers. The WP7 designer rules by the way).

Below are some of the known issues. I hope I can fix them as soon as I can.

  • Not all standard Blackberry controls are implemented.
  • The existing control implementations don't take into account the OS version. The SDK controls behave differently from version to version.
  • The control styles don't exactly match the BB styles.
  • The application is not really extensible at this point. The app will allow the user to add custom control libraries in order to support a larger number of components. This will be done by developing new modules and because the module discovery is done using a configuration file, the module integration will be easy.
  • The T4 templates that generate the Java code are hardcoded to translate only a small number of controls. At this stage even in the user developed a new module with new controls and used it, those controls will not be used for the code generation (they will be saved though). Some sort of mapping files will need to be used here in order to make the T4 templates truly generic.
  • There are also some architectural problems. This is my first PRISM application. Even though I learned a lot by building it I know there are a lot of things that I could have done better. I plan to correct these in future releases.

Points of Interest

Even though the application is by no means finished I think it has great potential. It can be especially useful for beginning Blackberry developers by helping them write well structured code fast. The application can also be useful to experienced developers by shifting their focus from tweaking the UI to the actual business logic they need to implement.

I had lots of fun writing this application especially considering I did it in order to learn PRISM.

If you like the article and if you think the application will be useful to you please take a moment to vote and post your comments or sugestions.

History

  • 6/17/2012 - Initial release.
  • 6/19/2012 - Added module description.
  • 6/20/2012 - Added the region and view registration section
  • 6/24/2012 - Added the command communication section

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Florin Badea
Software Developer
Romania Romania
No Biography provided

Comments and Discussions

 
QuestionI have mixed feelings about this one PinmvpSacha Barber18-Jun-12 3:38 
AnswerRe: I have mixed feelings about this one PinmemberFlorin Badea18-Jun-12 5:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140916.1 | Last Updated 24 Jun 2012
Article Copyright 2012 by Florin Badea
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid