This project is read-only.

Pure WPF Shell with Inf.Layout, Inf.Interface, Inf.Library, Inf.Module

Jul 24, 2007 at 1:15 AM
Edited Jul 24, 2007 at 1:16 AM
I have had over 1 year experience using CAB/SCSF April 2006 and for the last 6 months have been using WPF. It has come time to combine these two and I thought SCSFContrib would be the best approach.

I am trying to create a project structure similar to what is created by SCSF May 2007 guidance package. I couldn't find any guidance package for SCSFContrib, so I used SCSF to create a WinForms Shell project as well as Inf.Interface, Inf.Layout, Inf.Library, and Inf.Module projects (Solution1) NOTE: abbrev Inf for Infrastructure.

I created a Windows Application (WPF) project and followed the documentation for implementing a shell and creating a deck workspace (Solution2). That all worked. Then came the task of moving over one by one the non-shell projects from Solution1 to Solution2. As I moved each over, I removed references to Microsoft.Practices.CompositeUI.WinForms and System.Windows.Forms and instead used SCSFContrib.CompositeUI.WPF, PresentationCore, PresentationFramework, and WindowsBase. In Inf.Layout I kept ShellLayoutViewPresenter but removed ShellLayoutView.cs and replaced it with a new WPF UserControl called ShellLayoutView.xaml. Everything compiles.

The problem that I am running into is that when Inf.Layout module's Load method runs, there are no Workspaces registered with the root WorkItem:

public override void Load()
{
base.Load();

// Add layout view to the shell
ShellLayoutView _shellLayout = _rootWorkItem.Items.AddNew<ShellLayoutView>();
rootWorkItem.WorkspacesWorkspaceNames.LayoutWorkspace.Show(shellLayout); // <--This line throws NullReferenceException
}

When I set a breakpoint at the exact same location in Solution1 then there is one DeckWorkspace named LayoutWorkspace.

The Shell project contains the following ShellWindow.xaml file:

<Window x:Class="ClientFramework.Infrastructure.Shell.ShellWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cab="http://schemas.scsfcontrib.com/extensions/wpf/wpfcab"
Title="Shell" Height="600" Width="800"
>
<StackPanel>
<cab:DeckWorkspace x:Name="LayoutWorkspace"/> //hardcoded name matches Inf.Interface.WorkspaceNames
</StackPanel>
</Window>


And code behind ShellWindow.xaml.cs:

public partial class ShellWindow : System.Windows.Window
{

public ShellWindow()
{
InitializeComponent();

}

}


I next added the SCSFContrib.CompositeUI.WPF project to Solution2 and set a breakpoint in FrameworkElementSmartPartStrategy.AddToWorkItem but that code never gets called.

Questions:
  • How does LayoutWorkspace get registered with the root WorkItem?
  • Am I incorrect in thinking that I can create a pure WPF Shell (with Inf.Layout, etc.) following the project structure from SCSF?
  • Are there examples out there that successfully show what I am trying to do?
  • Is there a guidance package available for SCSFContrib?

Thanks,

Mark

Jul 24, 2007 at 11:31 AM
Edited Jul 24, 2007 at 11:36 AM
Hi Mark

The main problem you are having is that fact that you are trying to use the SCSFContrib WPF extentsions with the SCSF, and out of the box this does not work. It is really designed to work with the CAB. I had a similiar experience, but there is a solution. If you are using ApplicationShellApplication as your base class for the shell, your App class becomes your shell rather than the Window, but unfortunately it knows nothing about the root work item, because it is not a form or window. The work around I implemented is as follows: First, create an new interface in the inf.interface assembly called IRootWorkItemApplication. It has one property

public interface IRootWorkItemApplication
{
WorkItem RootWorkItem { get; set;}
}

Alter your App class so that the declaration is as follows:

public partial class App : Application, IInitializeApplication, IRootWorkItemApplication

then declare a private member thus: private WorkItem _rootWorkItem; and implement the interface thus:

public WorkItem RootWorkItem
{
get
{
return _rootWorkItem;
}
set
{
_rootWorkItem = value;
}
}

Replace the SmartClientApplication class in the Inf.Library assembly (as it is form based) so that the class declaration looks like this

public class SmartClientApplication<TWorkItem, TShell> : ApplicationShellApplication<TWorkItem, TShell>
where TWorkItem : WorkItem, new()
where TShell : Application, IInitializeApplication, IRootWorkItemApplication, new()

Override the AfterShellCreated method in your base class as follows:

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

Shell.RootWorkItem = this.RootWorkItem;
}

Your ShellApplication class should now inherit from SmartClientApplication instead of ApplicationShellApplication as follows:

class ShellApplication : SmartClientApplication<WorkItem, App>

All other code in this class is unaltered.

Now your App class will know about the RootWorkItem object, and will be accessible from the App class. Now in the constructor of your main window, after initialisation, you can add the window (and therefore it's workspaces) to the rootworkitems items collection as follows:

//add this form to our Root WorkItem list
_workItem = ((App)App.Current).RootWorkItem;

_workItem.Items.Add(this, ViewNames.MainWindow);

and you will then be able to access the windows workspaces by name. e.g

_layoutWorkspace.Name = WorkspaceNames.LayoutWorkspace;

//create our shell layout and show it.
ShellLayoutView _shellLayout = _workItem.Items.AddNew<ShellLayoutView>(ViewNames.ShellLayoutView);

workItem.Workspaces[WorkspaceNames.LayoutWorkspace].Show(shellLayout);

And that is all there is to it. I would rather have the _rootWorkItem member and property in the App class as static, but that does not work when implementing an interface. Making it static would make it easier to access declaratively in xaml and would allow you to access the property using App.RootWorkItem. I am still in the process of tweaking this implementation, but what I have described works fine for my pure WPF app.

Cheers

Steve
Jul 24, 2007 at 6:04 PM
I followed your instructions but the Inf.Layout Module.Load method still has no workspaces registered. It appears that the ShellWindow.xaml class in the Shell project is not being created as the ctor is not being called. I have the App.xaml StartupUri set to ShellWindow.xaml.

In the code above, I put the code as follows:

ShellWindows.xaml.cs in Shell project

public partial class ShellWindow : System.Windows.Window
{
private WorkItem workItem;

public ShellWindow()
{
InitializeComponent();

workItem = ((App)App.Current).RootWorkItem;

workItem.Items.Add(this, "MainWindow");
_layoutWorkspace.Name = WorkspaceNames.LayoutWorkspace;
}

}



Module.cs in Inf.Layout

public override void Load()
{
base.Load();

// Add layout view to the shell
ShellLayoutView _shellLayout = _rootWorkItem.Items.AddNew<ShellLayoutView>();
_rootWorkItem.Workspaces[WorkspaceNames.LayoutWorkspace].Show(_shellLayout);
}


Any other suggestions would be greatly appreciated.

If you want the my source to look at contact me at "mark dot tucker at jda dot com"

Thanks,

Mark
Jul 25, 2007 at 11:26 AM
Mark

You are indeed right. That way I had implemented this in my original post only partially worked. I was not loading the layout dynamically through use of the ProfileCatalog, but was instead loading the layout in the Main Windows constructor. After reading your post, I decided to see if I could improve on this, and have managed to come up with a better solution that works as expected, if not slight hacky!

The first thing to do is to modify the SmartClientApplication class definition I described yesterday from this

public class SmartClientApplication<TWorkItem, TShell> : ApplicationShellApplication<TWorkItem, TShell>
where TWorkItem : WorkItem, new()
where TShell : Application, IInitializeApplication, IRootWorkItemApplication, new()

to this:

public class SmartClientApplication<TWorkItem, TShell, TWindow> : ApplicationShellApplication<TWorkItem, TShell>
where TWorkItem : WorkItem, new()
where TShell : Application, IInitializeApplication, IRootWorkItemApplication, new()
where TWindow : Window, new()
{
private TWindow _window;

The class now has three generic identifiers, the third being the type of the MainWindow. We also declare a variable of type TWindow.

Next alter the AfterShellCreated method to something like the following:

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

Shell.RootWorkItem = this.RootWorkItem;

_window = new TWindow();

RootWorkItem.Items.Add(_window, ViewNames.MainWindow);
}

The constructor of the MainWindow class should now only have the following:

InitializeComponent();
_layoutWorkspace.Name = WorkspaceNames.LayoutWorkspace;

So we are now adding our MainWindow to the RootWorkItems collection earlier. That means that now when Module initialises, it will know about the layout workspace and will all load the layout correctly.

However, to get out MainWindow to display takes a little more work. The trouble is that when the App starts propert (via Shell.Run), it will create a new instance of the Window as specified by the StartupUri property in the App.xaml file. There is no way to set this null programatically to override this. The solution then is to create a new proxy empty Window, and set that to be your StartupUri instead. In the Load method of that window, simply close it.

Alter your App.xaml to something like this;

<Application
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Namespace.Shell.App"
StartupUri="Proxy.xaml"
>

To load our MainWindow, override the Start method in the SmartClientApplication class thus:

protected override void Start()
{
Shell.Run(_window);
}

This will cause the App to start up, loading the instance of the TWindow class we created in the AfterShellCreated method override. However, an instance of the Proxy window will also be created and loaded due to it being declared as our StartupUri. We therefore simply close it as soon as it is loaded.

public Window()
{
InitializeComponent();
this.Loaded += new RoutedEventHandler(Window_Loaded);
}

void Window_Loaded(object sender, RoutedEventArgs e)
{
this.Close();
}
And you are left with your MainWindow displayed.

This will all happen very quick, so that you will not see the proxy window before it is closed. However, so ensure this, you can always make very small and set it's opacity to zero, making it invisible.

If you still have problems after this, I can email you some source code if nececssary.

Cheers

Steve

Sep 6, 2007 at 11:19 PM
We are currently in the process of using WPF/CAB to start a brand new application and we only have basic knowledge on both technologies.
I've managed to start a shell using CAB in WPF but I'm unable to move on to get everything works under SCSF.

Seems that you already managed to fix this issue, do you have any sample code which you can share with us that run a WPF shell under SCSF?

Regards
Pierre
Sep 11, 2007 at 8:02 AM
Hi Pierre,
It seems that I'm in the similar situation to you. I'm worrying about the same question: how to host my WPF views into a WPF shell window other than the WinForm shell provided by SCSF.

Maybe you can have a look at the "Smart Client Software Factory WPF Support"(http://www.codeplex.com/SCSFWPF). This package seems the right solution that we need. But I've met some issue and have raised a discussion(http://www.codeplex.com/SCSFWPF/Thread/View.aspx?ThreadId=14967) thread.

Can you try it and reply that discussion with you feedback? Thanks!

Best regards,
XGardener


pmagna wrote:
We are currently in the process of using WPF/CAB to start a brand new application and we only have basic knowledge on both technologies.
I've managed to start a shell using CAB in WPF but I'm unable to move on to get everything works under SCSF.

Seems that you already managed to fix this issue, do you have any sample code which you can share with us that run a WPF shell under SCSF?

Regards
Pierre

Sep 12, 2007 at 2:24 AM
In the meantime, we took SCSFContrib V1.0 and change the source code of Infrastructure.* + Shell to get a pure WPF shell.
If you still interested, you can send me an e-mail and I'll be happy to send you our step-by step document that explain you how to do it.

Thanks
Pierre


xgardener wrote:
Hi Pierre,
It seems that I'm in the similar situation to you. I'm worrying about the same question: how to host my WPF views into a WPF shell window other than the WinForm shell provided by SCSF.

Maybe you can have a look at the "Smart Client Software Factory WPF Support"(http://www.codeplex.com/SCSFWPF). This package seems the right solution that we need. But I've met some issue and have raised a discussion(http://www.codeplex.com/SCSFWPF/Thread/View.aspx?ThreadId=14967) thread.

Can you try it and reply that discussion with you feedback? Thanks!

Best regards,
XGardener


pmagna wrote:
We are currently in the process of using WPF/CAB to start a brand new application and we only have basic knowledge on both technologies.
I've managed to start a shell using CAB in WPF but I'm unable to move on to get everything works under SCSF.

Seems that you already managed to fix this issue, do you have any sample code which you can share with us that run a WPF shell under SCSF?

Regards
Pierre


Oct 22, 2007 at 11:46 AM
Steve,
I've used a similar approach to what you describe above. One comment: you can use the startup property of the Application and point to an empty method instead of using the startupUri property and using a dummy window which should be closed

Acer
Oct 22, 2007 at 4:19 PM
First of all, I have not read through the above messages in their entirety, so I apologise if this is not what you wanted.

By the sounds of it, the problem is that the root/main/shell window is not being created by ObjectBuilder. It is instead being created by WPF. Therefore, it is not being added to the WorkItem, is not having dependencies injected, and the workspace is not being automatically added to the WorkItem etc.

What you need to do is ensure that it is CAB creating the shell window, not WPF. There are examples of this in the latest download (under the Samples/WPFCAB/ApplicationShellApplication folder). Make sure your App.xaml does not have StartupUri set. If it does, WPF will create the window, not CAB. Instead, you should create the window yourself in your root work item:

MainWindow mainWindow = Items.AddNew<MainWindow>();
 
//here we ensure that the WPF application is configured to shut down when mainWindow is closed (you may like to change
//this behavior in your application)
Application.Current.MainWindow = mainWindow;
Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
 
//show the main window
mainWindow.Show();

See the sample for more detail. Again, apologies if this does not reflect the exact problem being discussed in this thread.

Kent
Oct 26, 2007 at 2:49 PM
Hi all. I have only just picked up again on this thread as it lay dormant for a while. Although there is of course nothing wrong with Kent's solution, I do feel that my SmartClientApplication approach has it merits. SmartClientApplication is a reusable entity, and takes as it's third generic parameter a Window type, which allows for the passing of any base Window. It also avoid's the need to call Window.Show(), and instead relies on workings of the CAB to display the form by calling RootWorkItem.Items.Add(window, ViewNames.MainWindow); in the AfterShellCreated event and then Shell.Run(window); in the Start() method. The call to Add could be replaced with AddNew instead, rather than creating a new instance of the main Window explicility. I would hazard that the merits of using one version over another are neglible however. More a matter of taste.

As a point of interest, I have moved on a lot since this first discussion, and have recently been working on changing the DeckWorkspace. so that I can specify various Transions to apply to any SmartParts are loaded into the workspace. Something that Acropolis supports. If anyone would like some more info on this, I will be happy to pass this along.

Steve
Dec 19, 2007 at 10:27 PM
Hi Steve,

Thanks for your info, this had me puzzled until I found this email thread. I pretty much followed your example to get this working however I made a few modifications as follows

I constrained the TWindow template parameter in SmartClientApplication so it had to implement a simple interface IIdentifiableWindow. This interface has one simple property

public interface IIdentifiable
{
string Id { get; }
}

In SmartClientApplication::AfterShellCreate() I now have

TWindow main_window = new TWindow();
RootWorkItem.Items.Add(mainwindow, mainwindow.Id);

This is really only semantical sugar but I preferred it to using a constant defined somwhere. Obviously my MainWindow class now also implements IIdentifiableWindow and also has a static method which provides access to the same id, like you I set the name of the workspace in the constructor.

I found no reason to do the Proxy window 'hack', Instead I followed the advice of Kent and removed the StartupUri from App.xaml and created my own RootWorkItem class and provided an override for the OnRunStarted() method as follows:

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

MainWindow main_window = this.Items.Get<MainWindow>(MainWindow.WindowId); // WindowId is the static method that return the window id.

Application.Current.MainWindow = main_window;
Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;

main_window.Show();

This all seemed to work just fine.

Thanks for getting me on the right tracks.

Simon


ssolomon wrote:
Hi all. I have only just picked up again on this thread as it lay dormant for a while. Although there is of course nothing wrong with Kent's solution, I do feel that my SmartClientApplication approach has it merits. SmartClientApplication is a reusable entity, and takes as it's third generic parameter a Window type, which allows for the passing of any base Window. It also avoid's the need to call Window.Show(), and instead relies on workings of the CAB to display the form by calling RootWorkItem.Items.Add(window, ViewNames.MainWindow); in the AfterShellCreated event and then Shell.Run(window); in the Start() method. The call to Add could be replaced with AddNew instead, rather than creating a new instance of the main Window explicility. I would hazard that the merits of using one version over another are neglible however. More a matter of taste.

As a point of interest, I have moved on a lot since this first discussion, and have recently been working on changing the DeckWorkspace. so that I can specify various Transions to apply to any SmartParts are loaded into the workspace. Something that Acropolis supports. If anyone would like some more info on this, I will be happy to pass this along.

Steve