WPF For The WinForms Developer: Part 6

Multiple Windows

 There are several ways of navigating between WPF Windows. Many of the articles and books on WPF seem to home in on the WPF feature of owned forms. Although the ability to own Windows Forms has been available since the early days, I don't think it's a feature that has had much prominence in .NET tutorials. It seems to be flavor of the month in WPF however and it's an approach that you may want to look at later.

 First though it might be interesting to investigate if we can continue to use the same approaches that we have used in Windows Forms now that we are dealing with WPF Windows. Let's add a second Window to the project and see the options.

 In Orcas Beta 1 the 'Add New Item' menu pane looks like this:

Add New Window

 If you are using the VS 2005 WPF Extensions it is slightly different, but equally straightforward.

 The resulting display in the Visual Studio IDE is pretty much the same as for the original initial window, Window1.

Window2 in VS IDE

 At the top of the screen you have the WPF Design pane - the Window has the single celled Grid docked in it. The bottom part of the screen has the XAML Code pane, and the XAML code so far comprises the declaration of the Window itself, a class declaration, a couple of namespace declarations, some properties for the Window - Title, Height and Width - and finally the declaration of the Grid.

 WPF Window Default XAML Code

Before we go on to the look at access between Windows this might be a good time to look at them before we move on, as we skipped over this the first time round. If you are just interested in showing and accessing multiple windows at this stage, feel free to skip to the next section.

 The XAML code you see in the screenshot is created by default each time you add a new WPF Window to a project.

 The first line:

<Window x:Class="Window2"

creates a class in the XAML code named "Window2". The underlying purpose of this class is to enable the compiler to link the XAML code to the VB code-behind. Remember the use of the "x" prefix we used earlier when adding a Name property to a control so that the control appears in the xaml.vb code-behind combo of available controls? Without this class declaration you wouldn't be able to wire up the XAML UI to VB event code.

 If you were to delete the class declaration you wouldn't get an error message. Well... not until you tried to do any kind of event handling, that is. Note that if you were to delete that code the automatically created Window2.xaml.vb file still remains in existence and is still visible in the Solution Explorer. However, it would be the boniest of skeletons as there would be no Window events available from the right hand combo:

No Window Events available

Additionally, another important file, Window2.g.vb, is not created. This file is not shown in Solution Explorer in the Beta 1 edition, even with "Show All Files" selected. The xaml.vb file and the g.vb file contain the two parts of the Partial Class, in this case the class named "Window2".

 Window2.g.vb is created automatically and can be accessed where it sits in the Project's obj/Debug folder. The typical Window g.vb file looks like this:

'------------------------------------------------------------------------------
' <auto-generated>
'     This code was generated by a tool.
'     Runtime Version:2.0.50727.1318
'
'     Changes to this file may cause incorrect behavior and will be lost if
'     the code is regenerated.
' </auto-generated>
'------------------------------------------------------------------------------

Option
Strict Off
Option
Explicit On

Imports
System
Imports
System.Windows
Imports
System.Windows.Automation
Imports
System.Windows.Controls
Imports
System.Windows.Controls.Primitives
Imports
System.Windows.Data
Imports
System.Windows.Documents
Imports
System.Windows.Ink
Imports
System.Windows.Input
Imports
System.Windows.Markup
Imports
System.Windows.Media
Imports
System.Windows.Media.Animation
Imports
System.Windows.Media.Effects
Imports
System.Windows.Media.Imaging
Imports
System.Windows.Media.Media3D
Imports
System.Windows.Media.TextFormatting
Imports
System.Windows.Navigation
Imports
System.Windows.Shapes


'''<summary>
'''Window1
'''</summary>

<Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>  _
Partial
Public Class Window1
    Inherits System.Windows.Window
    Implements System.Windows.Markup.IComponentConnector

    Friend WithEvents GoButton As System.Windows.Controls.Button

    Friend WithEvents Label1 As System.Windows.Controls.Label

    Friend WithEvents GradientEllipse As System.Windows.Shapes.Ellipse

    Private _contentLoaded As Boolean

    '''<summary>
    '''InitializeComponent
    '''</summary>
    <System.Diagnostics.DebuggerNonUserCodeAttribute()>  _
    Public Sub InitializeComponent() Implements System.Windows.Markup.IComponentConnector.InitializeComponent
        If _contentLoaded Then
            Return
        End If
        _contentLoaded = true
        Dim resourceLocater As System.Uri = New System.Uri("/HelloWPF1;component/window1.xaml", System.UriKind.Relative)
        System.Windows.Application.LoadComponent(Me, resourceLocater)
    End Sub

    <System.Diagnostics.DebuggerNonUserCodeAttribute(),  _
     System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never),  _
     System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")>  _
    Sub System_Windows_Markup_IComponentConnector_Connect(ByVal connectionId As Integer, ByVal target As Object) Implements System.Windows.Markup.IComponentConnector.Connect
        If (connectionId = 1) Then
            Me.GoButton = CType(target,System.Windows.Controls.Button)
            Return
        End If
        If (connectionId = 2) Then
            Me.Label1 = CType(target,System.Windows.Controls.Label)
            Return
        End If
        If (connectionId = 3) Then
            Me.GradientEllipse = CType(target,System.Windows.Shapes.Ellipse)
            Return
        End If
        Me._contentLoaded = true
    End Sub
End
Class


 The next two lines of XAML code:

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

two XAML Namespaces are defined. For now, it's enough to know that the first of these namespaces contains the core WPF classes. The second one maps to the XAML namespace. Note the "x=" prefix used when pointing to this namespace. This is the key which allows you to insert those "x:..." code items that link your XAML code to the VB code-behind.

 The following line:

Title="Window2" Height="300" Width="300">

will now be familiar to you as it simply sets three basic properties of the Window. (I think I've previously mentioned that the Caption/Text property of the Window - i.e. the text that appears in the Windows Title Bar - is now known as the Title property.

With this second Window now created and available in the Visual Studio IDE let's look at some ways of linking it to the first one.

 Showing A Second Window

 One of the first questions I asked was "Can I still use the Show and ShowDialog methods that I'm familiar with from WinForms?" - i.e.:


Private Sub GoButton_Click(ByVal sender As Object, _
          ByVal e As System.Windows.RoutedEventArgs) Handles GoButton.Click

        Dim W2 As New Window2
        W2.Show()

End
Sub


  The answer ? :-

Window2 Shown using Show method

  Yes!

 So, for basic Show and ShowDialog actions you can stick to the WinForms approach that you will be familiar with.

 And what about the familiar problem of referencing the various windows from each other? Can we still use the WinForms approaches for this too?

Again, yes. One popular way is to add a module to the project and create variables with Friend scope. Something along these lines:

Code Copy
Module Module1

    Friend MyWin1 As Window1
    Friend MyWin2 As Window2

End
Module

 WPF Windows don't have a Load event; their nearest equivalent is the Loaded event. So you can use this to assign Window1 to the MyWin1 variable:

Code Copy
Private Sub Window1_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
        MyWin1 = Me
End Sub

thus enabling you to access this startup window from any other window in the project (as we will see shortly).

 In a similar way you can now edit the Window1 GoButton's click event so that a new instance of Window2 is created and assigned to that previously prepared MyWin2 variable. This approach again means that Window2 can be accessed from anywhere within this project:

Code Copy
Private Sub GoButton_Click(ByVal sender As Object, _
          ByVal e As System.Windows.RoutedEventArgs) Handles GoButton.Click

        MyWin2 = New Window2
        With MyWin2
            .ExitButton.Content = "Yikes - It works!"
            .Show()
        End With

End Sub

 Clearly, you need to add the ExitButton to Window2 before this will compile. Either drag one from the Toolbox to the Design pane or create one in XAML yourself.

 And as you already have a Friend variable holding a reference to Window1, you can pass something back to that Window from Window2. Maybe a message to be shown in that currently redundant Label:

Code Copy
    Private Sub ExitButton_Click(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles ExitButton.Click
        MyWin1.Label1.Content = "Window2 has been accessed"
        Me.Close()

    End Sub

 So the bottom line on multiple windows is that you can continue to use most of the approaches you have used for WinForms in the past. As far as I can tell, the default form instance - much loved by Classic VB developers, dropped (to a chorus of Boos) in VB.NET 2002 and 2003, and reinstated in VB 2005 - is not available in the same way in WPF. (So it is still available in standard Windows Forms in VB 2008 if that's an approach that you like to use).

 As we will see later, there are several other multiple display options exclusively available in WPF.

 Label Limitations
 There is one slight glitch with the code snippet above. If you actually run it exactly as-is you will find that the text in Label1 is truncated.

 

Of course, this is an easily surmountable problem. We could increase the width of the label or insert a carriage return in the text or adopt one of several other fixes. The reason I created this problem was simply to highlight a minor shortcoming in the WPF Label control. Unlike its WinForms Label cousin it doesn't have an Autosize property.

 This being WPF, of course, with its enhanced layout mindset the label has a default width setting of "Auto". If the form was made to be slightly wider - either by the developer or by the user - then the label will stretch in proportionate to the grid cell in which it is contained and all the text will eventually become visible:

 This might not always be an acceptable fix. So let's see what WPF can offer us as an alternative.

  The anwer is the WPF TextBlock. The name is self-explanatory. This control is not available from the Toolbox by default (at least not in the Beta 1 version). Attempting to add it in the usual way by selecting "Choose Items.." from the Toolbox unfortunately kept crashing the IDE in my Beta. I'm sure this will be fixed in upcoming versions. However, with our newly found XAML skills we can quickly and easily create one in code.

Code Copy
<TextBlock
       Grid
.Column="1" Grid.Row="0"
       Name="InfoTextBlock"
       Margin    ="32,28" Padding="6"
       Foreground ="#FFFF0000" Background="AliceBlue"
       FontSize="14"
       TextWrapping ="Wrap" HorizontalAlignment="Stretch"
       TextAlignment
="Center">
</TextBlock>

 Just to remind you, it isn't necessary to hand code all the XAML shown. Once you have entered the TextBlock start and close tags you can set properties in the IDE's Property Window if that is your preferred way of working. I'm surprised to find that already I am just as happy to hand code it - this may be partly because the Properties Window in Beta1 is not very intuitive. I do miss that alphabetical listing!

 As you can see from the snippet above, the TextWrapping property is available for the TextBlock and this is set to "Wrap".

We now change the code in Window2 so that it pastes the text message into this block: (I've added an occurrence counter just to make the extra effort worthwhile and to extend the length of the string even further) :-

Code Copy
Public Shared ShownCounter As Integer

    Private Sub ExitButton_Click(ByVal sender As Object, ByVal e As
             System.Windows.RoutedEventArgs) Handles ExitButton.Click

        ShownCounter += 1
        MyWin1.InfoTextBlock.Text = String.Format("Window2 has now been
        accessed {0} times."
, ShownCounter.ToString)

        Me.Close()

  End Sub

  And the TextBlock will display our text neatly wrapped and centered.

TextBlock with wrapped text.

  There are still some settings, e.g. minimum sizes that we could change so that the text remains visible even when the form size is decreased but I will leave that for you. We have already done something similar with other controls.

  If you are wondering if it is possible to put a border round the TextBlock, the answer is of course that it can be done ... and of course can be achieved in more than one way. We will cover this in a later section.


    Last updated 2nd August 2007