HowTo: avoid CAB TabWorkspace flickering
February 27th, 2006
Have you ever tried putting a TabControl in a Winforms app and create a new TabPage via code? Just do a goolge-groups search and check how many had the same annoying problem.
The problem is inside the Winforms
TabControl implementation. CAB
TabWorkspace is a simple wrapper
of this control and it has nothing to do with the
problem.
Avoid the flickering in the TabControl is achieved using the WM_SETREDRAW message (the first time I see this windows message was in delarou’s blog, and surprisingly he was talking about the CAB DeckWorkspace). This windows message
should be sent to the TabPage handle
to “tell him†not to redraw before adding it to the TabControl
(tabControl.TabPages.Add). Sending the message to the TabControl won’t have the desired results
however.
A common approach would be to
inherit from TabWorkspace,
override OnShow and send this
message before and after the base.OnShow. However we don’t have access
on the inherited class to the raw TabPage that will be added.
Because of this, the solution would
be to create a copy or modify the CAB TabWorkspace. The SetTabProperties method needs to be changed
const int WM_SETREDRAW = 0xB;
[DllImport("User32")]static extern bool SendMessage(IntPtr hWnd, int msg, int wParam, int lParam);
private void SetRedraw(IntPtr handle, bool on){ SendMessage(handle, WM_SETREDRAW, on ? 1 : 0, 0);}
private void SetTabProperties(TabPage page, TabSmartPartInfo smartPartInfo){ page.Text = String.IsNullOrEmpty(smartPartInfo.Title) ? page.Text : smartPartInfo.Title;
try { // Avoid flickering SetRedraw(page.Handle, false); TabPage currentSelection = this.SelectedTab; callComposerActivateOnIndexChange = false; if (smartPartInfo.Position == TabPosition.Beginning) { TabPage[] pages = GetTabPages(); this.TabPages.Clear(); this.TabPages.Add(page); this.TabPages.AddRange(pages); } else if (this.TabPages.Contains(page) == false) { this.TabPages.Add(page); }
// Preserve selection through the operation. this.SelectedTab = currentSelection; // Avoid flickering SetRedraw(page.Handle, true); } finally { callComposerActivateOnIndexChange = true; }}
Download CompositeUIExtensions.UI
As a final note, I plan to write the TabWorkspace hide feature. This
annoyance also has to do with the TabControl implementation (kzu opened a bug
and msft said it wouldn’t be fixed because of compatibility issues).
However, it could be workarounded writing some code on the TabWorkspace.
Stay tuned!
HowTo: load CAB modules on demand
February 27th, 2006
This article describes an approach to loading CAB modules on demand. You can Download the solution.
Introduction
Modules are composed of a set of services, WorkItems, SmartParts, controllers/presenters, business entities, and last but not least Module Initialization class, which is used for initializing and running the module’s WorkItems
When the application starts, CAB loads the ProfileCatalog.xml file and determines which modules it needs to load. After it retrieves the list of modules, it begins loading them and initializing them. If any of the modules contain a ModuleInit class, it will construct the class and call the appropriate Load and/or AddServices methods.
Loading modules is an expensive operation because it entails loading assemblies, creating services, registering WorkItemExtensions, and calling ModuleInit methods.
Let’s analyze this in detail and how to mitigate the expensive task.
Enumerating modules
CAB has a built-in service that provides module enumeration: the IModuleEnumerator. This service definition is very simple. It returns an array of module information (assembly name, roles and update location):
public interface IModuleEnumerator{ /// /// Gets an array of enumerated from the source the /// enumerator is processing. /// /// An array of instances. IModuleInfo[] EnumerateModules();}
The default implementation is the FileCatalogModuleEnumerator. You can use the FileCatalogModuleEnumerator class to read the catalog of modules from an XML file specified in the application configuration file. If no file is specified, the class searches for a file named ProfileCatalog.xml in the application base folder. Here is a sample ProfileCatalog file:
<SolutionProfile xmlns="http://schemas.microsoft.com/pag/cab-profile> <Modules> <ModuleInfo AssemblyFile="Module1.dll"> <Roles> <Role Allow="Users"/> </Roles> </ModuleInfo> <ModuleInfo AssemblyFile=" Module2.dll"> <Roles> <Role Allow="Sales"/> <Role Allow="Administrators"/> </Roles> </ModuleInfo> </Modules></SolutionProfile>
<SolutionProfile xmlns="http://schemas.microsoft.com/pag/cab-profile> <Modules> <ModuleInfo AssemblyFile="Module1.dll"> <Roles> <Role Allow="Users"/> </Roles> </ModuleInfo> <ModuleInfo AssemblyFile=" Module2.dll"> <Roles> <Role Allow="Sales"/> <Role Allow="Administrators"/> </Roles> </ModuleInfo> </Modules></SolutionProfile>
<SolutionProfile xmlns="http://schemas.microsoft.com/pag/cab-profile> <Modules> <ModuleInfo AssemblyFile="Module1.dll"> <Roles> <Role Allow="Users"/> </Roles> </ModuleInfo> <ModuleInfo AssemblyFile=" Module2.dll"> <Roles> <Role Allow="Sales"/> <Role Allow="Administrators"/> </Roles> </ModuleInfo> </Modules></SolutionProfile>
Loading modules
Now that we know how to enumerate the available modules, let’s look at how CAB loads them. The IModuleLoaderService is the service in charge of this:
public interface IModuleLoaderService{ /// /// Returns a list of the loaded modules. /// IList LoadedModules { get; } /// /// Loads the specified list of modules. /// /// The that will host the modules. /// The list of modules to load. void Load(WorkItem workItem, params IModuleInfo[] modules); /// /// Loads assemblies as modules. /// /// The that will host the modules. /// The list of assemblies to load as modules. void Load(WorkItem workItem, params Assembly[] assemblies); /// /// The event that is fired when a module has been loaded by the service. /// event EventHandler<DataEventArgs> ModuleLoaded;}
The default implementation is the ModuleLoaderService and performs these steps:
- Uses the information returned from the IModuleEnumerator to determine which modules should be loaded at run time.
- Loads the specified assemblies, checks them for any ModuleDependency attributes and builds a list of modules to initialize.
- It then iterates over each module in the sequence, loading the services associated with the module (including creating instances of classes decorated with the [Service] attribute)
- It iterates again over the whole sequence calling the Load method on any classes that extend ModuleInit.
- Registers any types with the WorkItemExtension attribute into the WorkItemExtensionService.
- Finally, it notifies about loaded modules.
Loading on demand
Now that you know how modules are loaded let’s see how we can change the default behavior to load only the necessary modules on startup and load others on demand. The solution consists of having two profile catalogs and a bootstrap module called arbitrary HomeModule.
The ProfileCatalog.xml has the modules loaded on startup and the ProfileCatalogOnDemand.xml has the modules to be loaded on demand.
Now we need a way to enumerate the available modules to be loaded on demand. We will reuse the CAB FileCatalogEnumerator to achieve this. The schema of both the ProfileCatalogOnDemand xml and the ProfileCatalog are the same. The only thing we need to change is which catalog might be read. Fortunately, this service has a constructor that accepts a catalog file path.
new FileCatalogModuleEnumerator( "ProfileCatalogOnDemand.xml" )
new FileCatalogModuleEnumerator( "ProfileCatalogOnDemand.xml" )
new FileCatalogModuleEnumerator( "ProfileCatalogOnDemand.xml" )
The bootstrap module will host this service so we need to add it to the root work item of the module.
WorkItem.Services.Add( new FileCatalogModuleEnumerator("ProfileCatalogOndemand.xml") );
WorkItem.Services.Add( new FileCatalogModuleEnumerator("ProfileCatalogOndemand.xml") );
WorkItem.Services.Add( new FileCatalogModuleEnumerator("ProfileCatalogOndemand.xml") );
Finally, we might be able to consume this service from within any workitem, view, presenter in the hierarchy. In conjunction with the IModuleLoaderService, loading modules on demand from any part of the CAB application will be easy.
void LoadFirstAvailableModuleTest(){ IModuleInfo[] modules = _modEnumerator.EnumerateModules() _moduleLoaderService.Load(_workItem, modules[0]);}
Sample
The sample that demonstrates these concepts has been written following the guidelines of SCBAT. Indeed, the Guidance Package has been used to create modules and views.
This is the Shell and the HomeModule loaded. The HomeModule inserts a Modules-> Load Module… menu item.
When the Load Module command is executed, a dialog shows the modules available and the modules already loaded. We need to use the module enumerator and module loader services, so they are injected on the presenter.
[InjectionConstructor]public ModuleLoaderDialogPresenter ( [ServiceDependency] IModuleLoaderService loader, [ServiceDependency] FileCatalogModuleEnumerator modEnumerator, [ServiceDependency] WorkItem workItem ){ _moduleLoaderService = loader; _modEnumerator = modEnumerator; _workItem = workItem;}
Then we select the Module1 and load it. When ModuleLoaderService finish the process it will raise a ModuleLoaded event.
The Module1 adds a View on the right workspace.
What we can do from here?
This sample serves as a starting point to take module loading to a next step
- We could change the ProfileCatalogOnDemand schema and include configuration to load modules conditionally
- The module enumerator could be part of the Shell and get rid of the bootstrap module, however we need to be able to identify the IModuleEnumerator service to retrieve from the ObjectBuilder container.
Download the solution
Hosting CAB in a UserControl - In depth
February 26th, 2006
In the previous post,
I’ve described an approach to host a CAB application in Outlook, Word,
or any other .net-compatible application. In this post, I will analyze
the internals of CAB as well as the code I had to write to implement
that.
In a normal CAB application (FormShellApplication) the
Run method initialize the whole CAB infrastructure, the root work item and the
modules.
Every CAB Application inherits from the CabApplication
class. This is the “heart†of CAB, where all the initialization takes place.
The Run method of CabApplication could
be separated in three stages:
- These lines of code will add the ObjectBuilder builder
strategies, the required services, will build up and initialize the root
workitem and finally it will run it - The second part will execute the Start method. This method
is abstract and must be overridden in derived applications. The FormShellApplication
will override it in order to start the Winforms application using the
Shell as the main formprotected override void Start()
{
Application.Run(Shell);
}
This line of code will start
the message pump and the winforms application will start listening UI messages
until the shell is closed. When that happens, part 3 is executed - The disposal of the root workitem and any visualizer
configured happens here.
What happens when you want to host CAB in a UserControl
and not in a regular Form?
The problem is that the message pump has already been
created by the host application: either a Winforms (not designed with CAB), an
Outlook addin, a Word Document, etc. That means that the Start method
cannot execute Application.Run(Shell). However, if we don’t execute Application.Run,
the thread will keep executing and will dispose the workitem (part 3)
How this could be solved?
We need to delay the disposal of the root work item until we
decide it is ok to dipose it. To achieve this, a new workitem need to be
created. This root workitem will override the base Dispose(bool disposing)
method in order to have control over when the Dispose needs to be executed.
public class UserControlWorkItem
: WorkItem
{
private bool
_dispose = false;
public
void
DoDispose()
{
_dispose = true;
Dispose(true);
GC
.SuppressFinalize(this);
}
protected override
void Dispose(bool
disposing) {
if (_dispose) {
base.Dispose(disposing);
}
}
}
Then, the UserControlShellApplication will be defined
with the following code. Note the TWorkItem being of type UserControlWorkItem
and the TUserControlShell of type UserControl.
public class UserControlShellApplication : WindowsFormsApplication
where TWorkItem : UserControlWorkItem,
new()
where TUserControlShell : UserControl, new()
{
protected override
void Start()
{
}
public
void
Dispose() {
RootWorkItem.DoDispose();
}
public TUserControlShell UserControlShell {
get { return base.Shell; }
}
}
Finally, we need to host the CAB application in a
UserControl. This works just as a container and it is different from the
UserControl that will act as a Shell. The UserControl will host an instance of UserControlShellApplication
and will execute the Run method. Also, when the UserControlHost
is disposed it will also dispose the UserControlShellApplication that
will finally dispose the UserControlWorkItem.
public partial class UserControlHost : UserControl
where TUserControlShellApplication : UserControlShellApplication,
new()
where TUserControlShell : UserControl, new()
{
protected
virtual void Run()
{
_cabApp = new TUserControlShellApplication();
_cabApp.Run();
_cabApp.UserControlShell.Dock = DockStyle.Fill;
this.Controls.Add(_cabApp.UserControlShell);
OnStarted(EventArgs.Empty);
}
protected override void
Dispose(bool disposing)
{
if
(disposing) {
_cabApp.Dispose();
}
base.Dispose(disposing);
}
…
}
Note: as shown in part 3 the Visualizer is also
disposed. The same approach applies to the visualizer.
HowTo: Host a CAB application in Outlook, Word, Excel, or a common Winforms application
February 26th, 2006
Reusing is
a common requirement in any team or framework. However, we usually reuse at
business logic or general infrastructure level. The User Interface reuse is
more difficult to achieve. CAB architecture is loosely coupled enough to make
that possible.
The
following screenshots illustrates the bank teller quickstart application hosted
in Outlook, Word and a regular Winforms application.
Just two
lines of code [:)]
Instructions:
- Download the solution
- Extract
the solution and build it. If you experience problems, check that
the references to the CAB assemblies are correct.
Create
the shell and the CAB application
- Create a
new project to host the shell and the CAB application.The shell in a regular CAB application would be a Form. However if we
want to host the application in a different executable (Outlook, Word, IE,
etc), we already have the container. So the Shell will be a UserControl. - Add
references to CAB assemblies. - Create a
regular UserControl and place the workspaces, menus, toolstrip, statusbar,
and everything related to the shell. - If you
need to override the CAB events like AfterShellCreated or
AddBuilderStrategies, create a new class that derives from CompositeUIExtensions.Hosting.UserControlShellApplicationwhere TUserControlShell is the type of the UserControl created on the previous step
and TWorkItem is the root work item that
must derives from UserControlWorkItem.
Consume
a CAB module/app from Outlook/Word/External application
- Open your
project and add references to CompositeUIExtensions.Hosting and the CAB assemblies. Also
copy the project related assemblies (module assemblies for example) to the
application output directory. - Creates
an instance of CompositeUIExtensions.Hosting.UserControlHost and add it to the
correspondent Controls collection. The following code illustrates how;
UserControlHost plugin = new UserControlHost();…Controls.Add( plugin );
- Create
the ProfileCatalog.xml, configure it with the desired module/s and set theBuild Action to CopyAlways.
Download
the solution that shows how to host the BankTeller app inside Outlook, Word and
Winforms.
The future of EDRA…
February 15th, 2006
Published by patterns & pracites: the long awaited question