Master pages in Silverlight - Part I

by gleblanc 5. January 2009 14:22

One of best appreciated features of ASP.NET version 2 was the master page concept. There is no reason a Silverlight application could not benefit from master pages. So, let's see how to implement master pages in Silverlight : similar capabilities though different implementation.

Our first implementation will be basic but we will improve it in forthcoming posts.

A master page is a web page (here a Silverlight page) that consists of fixed parts and one (or more) content area. The content area can be changed, for instance following an action on a menu item.

In the following Silverlight application, the master page is made of a layer at the top (with an animated text), a menu (left part) and a status bar (bottom part). In the content area, it's possible to display (at one time) one of two XAML pages. We will show how to switch content in the content area and how to pass information from one XAML content page to another one.
Everything here is kept simple since our goal is just to present the master page concept applied to Silverlight. For your information, the background image in the first content page is a painting from Alfred Sisley while the background image in the second content page is a painting from Claude Monet.



Project / source code


The master page (Page.xaml) is implemented as a 3-row grid : a canvas (with animated text) in the first row, the menu (in a vertical StackPanel) + content area the second row and the status bar in the third row : 

 <Grid x:Name="LayoutRoot" Background="AliceBlue">
  <Grid.RowDefinitions>
   <RowDefinition Height="30" />
   <RowDefinition/>
   <RowDefinition Height="20" />
  </Grid.RowDefinitions>
  .....
  <Canvas x:Name="animatedText" Grid.Row="0" >
   .....
  </Canvas>
  <Grid Grid.Row="1" >
   <Grid.ColumnDefinitions>
    <ColumnDefinition Width="100" />
    <ColumnDefinition />
   </Grid.ColumnDefinitions>
   <StackPanel Grid.Column="0">
    .....
   </StackPanel>
   <Grid x:Name="PageContainer" Grid.Column="1" />
  </Grid>
  <TextBlock x:Name="stBar" Grid.Row="2" Text="This is the status bar"
             VerticalAlignment="Center" />
 </Grid>

The two content pages are created as normal Silverlight user controls : Page1.xaml and Page2.xaml. In these two Silverlight pages, we added some common controls, to make these pages more realistic. These three XAML files (Page.xaml for the master page and Page1.xaml and Page2.xaml for the content pages) are part of the same project. There are thus included in the same XAP file.
 
In the Page.xaml.cs file, we declare two properties, corresponding to the two content pages :

 public Page1 page1 { get; set; }
 public Page2 page2 { get; set; }

These two properties are initialized in the Page constructor. Page1 is then set as the initial content page :

 public Page()
 {
  InitializeComponent();
  page1 = new Page1();
  page2 = new Page2();
  PageContainer.Children.Add(page1);
 }

To make Page2 the current content page (thus to switch content page), we write :

 SwitchToPage(page2);

with SwitchToPage which is (additionally, we display a message in the status bar) :

 public void SwitchToPage(UserControl p)
 {
  Page masterPage = Application.Current.RootVisual as Page;
  UserControl currentPage = masterPage.PageContainer.Children[0] as UserControl;
  if (currentPage != p)
  {
   masterPage.PageContainer.Children[0] = p;
   stBar.Text = "At " + DateTime.Now.ToLongTimeString() + ", switch to page named " + p.Name;
  }
 }

To give a name to a content page, we just add a Name property in the UserControl tag.

To pass information from a content page to the master page or another content page, different solutions are possible. One of them consists in storing this information as a ressource. For instance, to initialize a field named InfoPage1 somewhere in Page1.xaml.cs

 Resources.Remove("InfoPage1");
 Resources.Add("InfoPage1", "new value of InfoPage1");

To read this information from another content page :

 Page p = Application.Current.RootVisual as Page;
 string s = p.page1.Resources["InfoPage1"] as string;


To switch to Page1 directly from Page2, we would write :

 Page p = Application.Current.RootVisual as Page;
 p.SwitchToPage(p.page1);
 

In the next posts, we will improve the page switching mechanism. See you soon.
 

Tags:

Synchronous animations

by gleblanc 3. December 2008 12:39

In Silverlight, animations are launched asynchronously. Great... But sometimes, this feature makes things more complicated than necessary, as experienced in the following program that graphically renders the behavior of a sort algorithm. So, let's explain how to simulate synchronous animations. Doing so, we can keep the algorithm as it is. We just have a few lines to add, to launch animations.


Project / source code

Our sort algorithm is the simplest one. We know it's not efficient but we don't care since our goal is just to present synchronous animations. This algorithm (to sort the array T of integers) is :
 private void Sort()
 {
  for (int i = 0; i < T.Length-1; i++)
  {
   int mini= i+1;
   for (int j = i + 2; j < T.Length; j++)
    if (T[j] < T[mini]) mini = j;
   if (T[i] > T[mini])
   {
    // exchange
    int c = T[i]; T[i] = T[mini]; T[mini] = c;
   }
  }
 } 

If we run the Sort function as it is and we add animations, the program will end in less than a millisecond. Nothing will be visible : since animations can only be run asynchronously, a lot of animations would be launched asynchronously, within that millisecond ! Adding Thread.Sleep instructions would slowdown the program but would also stop animations. In technical terms : we cannot run Sort from the main (also named UI) thread. We could create a thread (ie another path of code execution) and execute Sort on this thread but actions on UI elements (changing colors or animations for instance) must be run from the main thread.

So, let's explain how to solve this dilemma. We first create a thread (in the function that handles the Click event on the Sort button) and launch Sort on that secondary thread :
 using System.Threading;        
 .....
 Thread thread;
 .....
 private void bSort_Click(object sender, RoutedEventArgs e)
 {
  .....
  thread = new Thread(new ThreadStart(Sort));
  thread.Start();
 }

Now, in the Sort function, we need to add instructions to change colors of bars and launch animations. To do so, we write the Display function, that will also run on the secondary thread. Display accepts as arguments a string (that contains the operation),  two integers (to eventually specify concerned UI elements) and a boolean indicating a pause is needed :
 int[] T;
 .....
 private void Sort()
 {
  .....
  for (int i = 0; i < T.Length-1; i++)
  {
   Display("Candidate", i, 0, true);
   .....
   int mini= i+1;
   ..... 
   for (int j = i + 2; j < T.Length; j++)
   {
    Display("Comparing " + T[i] + " with " + T[j] , 0, 0, true);
    ......
    if (T[j] < T[mini])
    {
     .....
    }
   }
   if (T[i] > T[mini])
   {
    Display("Move1", i, mini, false);
    Display("Move2", mini, i, true);
    .....
  }
 }
 .....
 void Display(string s, int a, int b, bool bDelay)
 {
  .....
  if (bDelay) Thread.Sleep(DELAY);
 }
Sure, the Sort function has been modified but we just added lines. Some algorithms are so complicated that it's better not go further.

Accesses to UI elements are performed in the Display function. How ? First, we need to add a delegate. A delegate is nothing new for C programmers : it's a function pointer (in other words, a variable that holds the address of a function, enabling the function call thru that pointer). Here, our delegate "points to" a function with three arguments (a string and two integers) :
 private delegate void LaunchAnimationDelegate(string s, int a, int b);
 .....
 LaunchAnimationDelegate LaunchAnimDel;
 .....
 private void Sort()
 {
  LaunchAnimDel = new LaunchAnimationDelegate(LaunchAnimation);
  .....
 }
 void Display(string s, int a, int b, bool bDelay)
 {
  Dispatcher.BeginInvoke(LaunchAnimDel, new object[] { s, a, b });
  .....
 }
 .....
 private void LaunchAnimation(string s, int a, int b)
 {
  .....
 }

We first defined a type (LaunchAnimationDelegate), ie an information for the compiler. Then a variable (named LaunchAnimDel) of that type. That variable is initialized at the beginning of the Sort function. Finally, we write the function (named LaunchAnimation) to be called thru that delegate. That function will be executed on the main, UI, thread. To force execution on the main thread, the function call must be performed with Dispatcher.BeginInvoke. See also how parameters are passed to the function. In LaunchAnimation, we can have any instruction that modifies UI elements, for instance
 tabRect[a].Fill = new SolidColorBrush(Colors.Orange);

Concerning the visual interface, let's say we have a Grid split into four lines. The second line (with the animation) is a Canvas that contains 10 Rectangle (height and color are changing but not position). We created two other rectangles for the Move animations, with storyboards created dynamically (at Loaded time). To keep code as easy as possible but have the bars growing from bottom to top, a ScaleTransform (with Y equals to -1) is performed on the Canvas. Source code is available, as usual.

Tags:

Menus, part V : Sliding menu

by gleblanc 25. November 2008 02:06

We continue our serie of animated menus, this time : the sliding menu.


Project / source code

As usual, our sliding menu is declared in a XML file, Menu.xml, inserted as a resource :
  <?xml version="1.0" encoding="utf-8" ?>
 <Menu>
  <Item Text="Italy" >
   <Item Miniature="MiniVenice.jpg" Id="11" Text="Venice, Grand Canal"/>
   .....
   <Item Miniature="MiniVatican.jpg" Id="15" Text="Vatican, St. Peter's Square" />
  </Item>
  <Item Text="USA" >
   <Item Miniature="MiniYosemite.jpg" Id="21" Text="Yosemite National park" />
   .....
   <Item Miniature="MiniGrand Canyon.jpg" Id="23" Text="Grand Canyon National Park"/>
  </Item>
  .....
 </Menu>

The user has just one constraint : reserve space for the menu on top of anything else in the window (this explains the Canvas.ZIndex) . Miniatures must be included as resources (they are displayed in sub-menus). The user can then handle the Click event :
<Grid x:Name="LayoutRoot" Background="White" >
  <Grid x:Name="menuGrid" Canvas.ZIndex="99" Height="40" VerticalAlignment="Top" >
   <gl:Menu Click="menuGrid_Click" />
  </Grid>
  <Grid x:Name="mainGrid" >
   .....
  </Grid>
 </Grid>

As we did for other menu, we build a Silverlight user control named Menu.xaml in the glMenu namespace. Our menu as a Grid containing a Canvas for the sub-menus and an horizontal StackPanel for the main menu items. Why a Canvas for the sub-menus ? They are animated (down animation for opening and up animation for closing) and it is not (unfortunately) possible to animate the Margin property :
<Grid x:Name="MenuRoot" >
  .....
  <Canvas x:Name="canSubMenus" />
  <StackPanel x:Name="spMainMenu" Orientation="Horizontal" Loaded="spMainMenu_Loaded" >
   ....
  </StackPanel>
 </Grid>

Since main menu items and sub-menu items are quite different, we create two classes : MainMenuItem and SubMenuItem. In memory, we also keep a list of main menu items and a table for sub-menu items (the number of entries in this table depends on the number of main menu items) :
 List<MainMenuItem> lstMainMenuItems;
 List<SubMenuItem>[] tabSubMenuItems;

The main menu is first build in the spMainMenu_Loaded function. Thanks to Linq for Xml, we analyze the XML file to find the main menu items :
lstMainMenuItems = new List<MainMenuItem>();
 XElement xmlRoot = XElement.Load("Menu.xml");
 getMainMenuItems(xmlRoot);
 .....
 void getMainMenuItems(XElement xml)
 {
  var lstItems = from p in xml.Elements("Item") select p;
  foreach (var el in lstItems)
   lstMainMenuItems.Add(new MainMenuItem {Text=el.Attribute("Text").Value, xElem=el});
 }

The main menu is then build using Silverlight UI elements : a Grid including a TextBlock (indeed two TextBlock to give a 3D effect to the display). For each main menu item, we handle the MouseEnter event (to make visible the corresponding sub-menu), the MouseLeave (to hide it) but also the MouseMove (leaving the main menu item thru the bottom side must leave the sub-menu as it is).

The sub-menus are then build (at this time, we know the number of sub-menu items). All the sub-menus are placed in a Canvas (canSubMenus). Each sub-menus (three here) are build on a canvas. Each sub-menu item (we have 5 for Italy and 3 for USA) are also based on a canvas containing an image (the miniature). For each of these canvas, we handle the MouseEnter, MouseLeave and MouseLeftButtonDown events.

Source code is included, as usual.

Tags:

Page turner updated for Silverlight 2 RTM

by gleblanc 17. November 2008 21:57

Mitsu Furuta (from Microsoft France) updated his Page Turner to Silverlight 2 RTM. Let's explain how to use it.

Let's first demonstrate the page turner, with paintings from Pablo Picasso (the Spanish-born painter is currently honored in Paris with several outstanding exhibitions). Please note that some pages are more than just images.


Project / source code

You can find Mitsu's component at the following address : www.codeplex.com/wpfbookcontrol. Codeplex is really becoming a major player ! Download the zip-file and unzip it. You will find SLMitsuControls.dll (a 35 KB file!) in the SLMitsuControls/ClientBin folder. The component is under Ms-Pl licence, meaning you can use it in your own products, even commercial, for free (no royaltie to Microsoft, even if your product is not free) but, of course, you can't claim rights on ownership.

Let's now create a Silverlight application. Add a reference to SLMitsuControls.dll (in the Silverlight part of the project, right-click on the project, Add Reference and Browse to the folder containing the SLMitsuControls.dll file).

Let's first assume that our book-like control just contains images. These images need to be copied in the ClientBin folder (the one containing the XAP file). We first need to populate our book with these images. We keep file names (without extension) in an array of string (in Page.xaml.cs) :
 string[] ts = {"Picasso", "Head", "Blind man", "At the Lapin Agile", "Chicks from Avignon",
                "Woman crying", "Guitar player", "Auto portrait"};

In the UserControl tag (in Page.xaml), we add a xmlns tag for the control (any other name than local is OK), handle the Loaded event and insert our book in the first row of the main grid (the second row will contain a status bar implemented as a TextBlock) :
 <UserControl x:Class="TurnThePage2.Page"
    ..... 
    xmlns:local="clr-namespace:SLMitsuControls;assembly=SLMitsuControls"
    ..... 
    Loaded="UserControl_Loaded" >
  <Grid x:Name="LayoutRoot" Background="LightBlue" >
   <Grid.RowDefinitions>
    <RowDefinition />
    <RowDefinition Height="25" />
   </Grid.RowDefinitions>
   <local:UCBook x:Name="book" Grid.Row="0" OnPageTurned="book_OnPageTurned" Margin="20" />
   .....
  </Grid>
 </UserControl>
In the Page.xaml.cs file, we need to add a using, declare that the Page class implements the IDataProvider interface and implement two functions (just two small lines, always the same) :
 .....
 using SLMitsuControls;
 .....
 namespace TurnThePage2
 {
  public partial class Page : UserControl, IDataProvider
  {
   ..... 
   public int GetCount()
   {
    return pages.Count;
   }
   public object GetItem(int index)
   {
    return pages[index];
   }
  }
 }
In case our pages are just images, it's possible to keep them in a List<Image>. But some of our pages will be more elaborate (a page could for instance be a complex grid). So, we prefer List<UIElement> since UIElement is the base class for UI elements. But here, our book is (to begin) just a collection of images (only two images here, no need to show more) :
 List<UIElement> pages;
 .....
 private void UserControl_Loaded(object sender, RoutedEventArgs e)
 {
  pages = new List<UIElement> {
   new Image {Source=new BitmapImage(new Uri(ts[1]+".jpg",
              UriKind.Relative)), Stretch=Stretch.UniformToFill },
   new Image {Source=new BitmapImage(new Uri(ts[2]+".jpg", UriKind.Relative)),
              Stretch=Stretch.UniformToFill }
   };
  book.SetData(this);
 }
It's enough to get a page turner for images ! It's possible to handle the OnPageTurned event :
 private void book_OnPageTurned(int leftPageIndex, int rightPageIndex)
 {
  .....
 }
After a page turn, the arguments give you the left and right pages that are visible : starting at 1 (be careful !) and with -1 for absence of page.

Let's now complicate a bit : our first page will present an animated text on top of a photo and the last page will present a button on top of an image (remember : the images must be copied in the ClientBin folder). Our downloadable project is still a bit more complex (with a list box). We dynamically specify attributes for the grid in first page and dynamically add a TextBlock :  
 private void UserControl_Loaded(object sender, RoutedEventArgs e)
 {
  pages = new List {
            new Grid (),    // first page
            new Image ..... // second page (image)
            .....
            new Image ....
            new Grid()      // last page
           };
  // specify first page
  Grid g = pages[0] as Grid;
  g.Background = new ImageBrush { ImageSource = new BitmapImage(new Uri("Picasso.jpg", 
                                  UriKind.Relative)),
                                  Stretch = Stretch.UniformToFill }; 
  TextBlock tb = new TextBlock { Text = "Pablo\nPicasso", FontSize = 70,
                    Foreground = new SolidColorBrush(Color.FromArgb(255, 173, 216, 230)),
                    TextWrapping = TextWrapping.Wrap,
                    VerticalAlignment = VerticalAlignment.Center, 
                    HorizontalAlignment = HorizontalAlignment.Center,
                    TextAlignment = TextAlignment.Center };
  .....    // here the animation on FontSize
  g.Children.Add(tb);
  ......
The code to animate the FontSize property is :
 Storyboard stbPage0 = new Storyboard();
 Storyboard.SetTarget(stbPage0, tb);
 DoubleAnimation daSize = new DoubleAnimation {From = 70, To = 0,
                                RepeatBehavior = RepeatBehavior.Forever,
                                AutoReverse = true };
 daSize.Duration = new Duration(new TimeSpan(0, 0, 5));
 Storyboard.SetTargetProperty(daSize, new PropertyPath("FontSize"));
 stbPage0.Children.Add(daSize);  
 stbPage0.Begin();

The last page (with a button on top of an image) is similar :
 g = pages[pages.Count - 1] as Grid;
 g.Background = new ImageBrush { ImageSource = new BitmapImage(new Uri("Picasso2.jpg",
                                 UriKind.Relative)), Stretch = Stretch.UniformToFill };
 Button b = new Button { Content = "Back to the first page", Width = 130, Height = 50 };
 b.Click += new RoutedEventHandler(b_Click); 
 g.Children.Add(b);

The function that handles the click being, as usual :
 void b_Click(object sender, RoutedEventArgs e)
 {
  ..... 
 }
To go back to the first page, we just need to write :
 book.CurrentSheetIndex = 0;
It's possible to launch an animation to switch to a next or previous page : 
 book.AnimateToNextPage(1000);
 .....
 book.AnimateToPreviousPage(1000);
where the argument is the duration of the animation, expressed in milliseconds.

Tags:

Menus, part IV : collapsible menu

by gleblanc 10. November 2008 17:23

In this post, we will implement a collapsible menu, animated of course. It's amazing to see how easily any .NET educated programmer (even WinForms only) can add functionnalities to Silverlight applications.


Project / source code

Our collapsible menu is declared in a XML file (Menu.xml, inserted as a resource) :

 <?xml version="1.0" encoding="utf-8" ?>
 <Items>
  <Item Text="China" Id="10" />
  <Item Text="USA" Id="30" >
   <Item Text="Grand Canyon" Id="31" />
   <Item Text="Yosemite"     Id="32" />
   .....
  </Item>
  <Item Text="Egypt" Id="40" />
  .....
 </Items>

To build the collapsible menu in our Silverlight application, we first add a Silverlight user control, named CollapsibleMenu (the CollapsibleMenu.xaml and CollapsibleMenu.xaml.cs files are then created by Visual Studio). We force the namespace named glMenu (gl for our own initials). In the CollapsibleMenu.xaml file, we suppress Width and Height properties. Business as usual for a user control.

Our collapsible menu is implemented as an horizontal StackPanel, with two elements : one (a vertical StackPanel with menu items) for the menu itself (horizontally collapsible) and a bordered TextBlock (22 pixels wide) for the tab (always visible, with "Menu" written vertically).

 <StackPanel Orientation="Horizontal" >
  <StackPanel x:Name="spMenu" Orientation="Vertical" ..... >
   .....
  </StackPanel>
  <Border CornerRadius="0,15,15,0" Width="22" VerticalAlignment="Top" ..... >
   <TextBlock x:Name="tab" Text="Menu" TextWrapping="Wrap" ...... />
  </Border>
 </StackPanel>

For the TextBlock (named tab), we also specify some font properties (FontFamily, FontSize, etc.) and handle three events : MouseEnter and MouseLeave but also MouseMove (leaving the tab thru the left or right sides must have a different effect : keeping the menu as it is or collapsing it).

The menu itself is a vertical StackPanel (with an initial width of 0 pixel) that will be populated by program in the function that handles the Loaded event :

 <StackPanel x:Name="spMenu" Orientation="Vertical" Width="0" Loaded="spMenu_Loaded"
             MouseLeave="spMenu_MouseLeave" >
   .....
 </StackPanel>

We handle the Loaded event (to initialize the menu from the XML file) and the MouseLeave event (to progressively reduce its width to 0). Inside this StackPanel, we define an animation (to horizontally and progressively expand or collapse the menu in one second) :

 <StackPanel x:Name="spMenu" ..... >
  <StackPanel.Resources>
   <Storyboard x:Name="stb" >
    <DoubleAnimation x:Name="da" Storyboard.TargetName="spMenu"
                     Storyboard.TargetProperty="Width" Duration="0:0:1" />
   </Storyboard>
  </StackPanel.Resources>
 </StackPanel>
Q

In the Loaded function, we read the XML file and prepare the menu (first as a list of MenuItem objects) and then (in the PrepareMenu function) as a set of XAML tags :

 List<MenuItem> lstMenuItems;
 private void spMenu_Loaded(object sender, RoutedEventArgs e)
 {
  lstMenuItems = new List<MenuItem>();
  XElement xmlRoot = XElement.Load("Menu.xml");
  getMenuItems(xmlRoot);
  PrepareMenu();
 }

The MenuItem class (also created in the glMenu namespace) is

 public class MenuItem
 {
  public MenuItem(string aText, int aId, int aLevel, int acountSubItems)
  {
   Text = aText; Id = aId; Level = aLevel; countSubItems = acountSubItems;
  }
  public string Text {get; set;}
  public int Id {get; set;}
  public int Level { get; set; }
  public int countSubItems {get; set;}
 }

Level is 0 for a main menu item (for instance USA) and 1 for a submenu item (for instance Yosemite). The field countSubItems contains the number of sub-items for a main menu item (5 for Italy).

Linq provides an incomparable help in the task of analyzing the XML file (do not forget to add a reference to System.Xml.Linq) :

 void getMenuItems(XElement xml)
 {
  var lstItems = from p in xml.Elements("Item") select p;
  foreach (var el in lstItems)
  {
   string Text = el.Attribute("Text").Value;
   int Id; Int32.TryParse(el.Attribute("Id").Value, out Id);
   if (el.HasElements == false)
    lstMenuItems.Add(new MenuItem(Text, Id, 0, 0));
   else
   {
    // this main menu item has one or several sub-menu items
    lstMenuItems.Add(new MenuItem(Text, Id, 0, el.Elements("Item").Count()));
    var subItems = from pSub in el.Elements("Item") select pSub;
    foreach (var sub in subItems)
    {
     Text = sub.Attribute("Text").Value;
     Int32.TryParse(sub.Attribute("Id").Value, out Id);
     lstMenuItems.Add(new MenuItem(Text, Id, 1, 0));
    }
   }
  }
 }

The menu is a vertical StackPanel. Each main menu item is a Grid. Each sub-menu item is also a Grid but included in a vertical StackPanel containing all sub-menu items :

 void PrepareMenu()
 {
  int N = lstMenuItems.Count;
  for (int n=0; n<N; n++)
  {
   MenuItem mi = lstMenuItems[n];
   PrepareMainMenuItem(mi);
   if (mi.countSubItems > 0)
   {
    PrepareSubMenuItems(mi);
    n += mi.countSubItems;
   }
  }
  spMenu.Width = 0;  // menu initially fully collapsed
  ..... // add storyboard to main items with submenu
 }

At the end of the PrepareMenu function, we also need to add a Storyboard (to vertically and progressively expand or callapse a sub-menu). This is only necessary for main menu items that have sub-menu items.

In the PrepareMainMenuItem function, we prepare the XAML for a specific main menu item : a Grid (with a linear gradient brush for Background) plus a TextBlock for the text. We handle the MouseLeftButtonDown (to open the associated sub-menu but it could be to activate a main menu item without sub-menu).

In the PrepareSubMenuItems function, we prepare the XAML for the sub-menu of a specific main menu item : a vertical StackPanel containing as many Grid as they are sub-menu items in this main menu item). This Grid is made of a Rectangle (for Background and Stroke) and a TextBlock.

As we did in previous posts, our collapsible menu is able to generate the Click event (the MenuEventArgs is just a class derived from EventArgs, plus the Id field as complementary information) :

 public delegate void MenuEventHandler(object sender, MenuEventArgs e);
 public event MenuEventHandler Click;
 ....
 MenuEventArgs args = new MenuEventArgs(); args.itemId = Id;
 if (Click != null)     // if Click event handled by the user
    Click(this, args);

In Page.xaml, the user specifies the collapsible menu with (do not forget to put it on top) :

<UserControl .....
    xmlns:gl="clr-namespace:glMenu"
    ..... >
    <Grid x:Name="LayoutRoot" Background="White">
      <Grid x:Name="menuGrid" Canvas.ZIndex="99"  >
       <gl:CollapsibleMenu x:Name="menu" VerticalAlignment="Top" Margin="0, 20, 0, 0"
                           Click="myMenu_Click" />
      </Grid>
      <Grid ..... >        here the main grid for the Silverlight application
       .....
      </Grid>
    </Grid>

That's all, folks !

Tags:

Menus, part III : animated multi-level drop-down menu

by gleblanc 2. November 2008 20:22
In this post, we will add (dynamically and by program) an animation to our previous drop-down and XML-driven menu. Just of few lines to add !


Project / source code

The artwork initially presented is "La montagne Sainte-Victoire" from Paul Cézanne.

As seen in the previous post, our sub-menus and menu items are dynamically created when needed. When a sub-menu is created, we just need to dynamically add a Storyboard object. The code here is added in our CreateSubMenu fonction, that accepts mnu, of type Menu, as argument. In our Menu class, we added a field, named stbShow and of type Storyboard :

 if (mnu.parentMenu != null)  // not applicable to the main (horizontal) menu
 {
  // dynamically create storyboard
  Storyboard stb = new Storyboard();
  Storyboard.SetTarget(stb, mnu.menuContainer);

  DoubleAnimation daHeight = new DoubleAnimation();
  daHeight.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 400));
  daHeight.From = 0; daHeight.To = mnu.lstMenuItems.Count * cellItemHeight;
  Storyboard.SetTargetProperty(daHeight, new PropertyPath("Height"));

  DoubleAnimation daWidth = new DoubleAnimation();
  daWidth.Duration = new Duration(new TimeSpan(0, 0, 0, 0, 400));
  daWidth.From = 0; daWidth.To = mnu.Width;
  Storyboard.SetTargetProperty(daWidth, new PropertyPath("Width"));

  stb.Children.Add(daHeight); stb.Children.Add(daWidth); 
  mnu.menuContainer.Resources.Add("stbShowHide", stb);
  mnu.stbShow = stb;
 }
 .....
 mnu.stbShow.Begin();

Just a few explanations : our menu is a Menu object (Menu being defined in our program). It's implemented as a Border container containing a vertical StackPanel. A reference to this Border container is kept in the menuContainer field of our Menu class. We define two DoubleAnimation to increase Height and Width properties from 0 to their final values in 400 millisec. Final Height depends on number of menu items (value given by the lstMenuItems list of menu items) and final Width depends on largest width for menu items (value kept in the Width field). The two DoubleAnimation are added as children of the Storyboard object just created. The Storyboard is then added as resource of the menu container (ie the Border container).

That's it ! 

Tags:

Menus, part II : multi-level drop-down menu

by gleblanc 26. October 2008 14:43

In this post, we will implement a multi-level drop down menu, ie eventually with any number of sub-menus. The drop-down menu presented here is XML driven, though without animation, as possible with Silverlight. We'll add animation effects in the forthcoming post.


Project / source code

The art-work initially displayed is "Tomorrow" from Jean-Yves Tanguy, a French painter who became an American citizen.

Our menu is XML-driven, as a set of Item tags. An Item tag can itself contain any number of Item tags (corresponding to any number of sub-menus and any number of sub-menu items) : 

<?xml version="1.0" encoding="utf-8" ?>
<Menu>
  <Item Id="10" Text="Antarctica" />
  <Item Id="20" Text="Europe" >
    <Item Id="201" Text="Italy" >
      <Item Id="2011" Text="Florence" />
      .....
    </Item>
    <Item Id="202" Text="France" >
      <Item Id="2021" Text="Paris" />
      <Item Id="2022" Text="French riviera" >
        <Item Id="20221" Text="Nice" />
        <Item Id="20222" Text="Monaco" >
          <Item Id="202221" Text="Monaco" />
          <Item Id="202222" Text="Monte-Carlo" />
        </Item>
        <Item Id="20223" Text="Saint-Jean Cap Ferrat" />
      </Item>
      <Item Id="2023" Text="Provence" />
    </Item>
    .....
  <Item Id="30" Text="America" >
  .....
  </Item>
  .....
  <Item Id="50" Text="Oceania" />
</Menu>

For each menu item, you need to provide an Id value as well as a Text. A Click event is raised whenever the user clicks on a menu item (the Id can then be retrieved from the second parameter of the event handling function).

As usual, we create a Silverlight application that will host the drop-down menu we are building here. Let's assume the application is named UseDropDownMenu. Visual Studio creates a namespace for the program and, by default, this namespace is named UseDropDownMenu, as the application. We first create a DropDownMenu XAML file and we specify glMenu as namespace (thus, as seen in the previous post, one change in the xaml file and another one in the xaml.cs file, these two files being created by Visual Studio).

In the hosting Silverlight application, we need to reserve space for the main menu (top horizontal bar, whose height is 30 pixels). The menu part must remain on top (this is why the Canvas.ZIndex attribute, applicable to a Grid container, is given a large value) :
 <UserControl x:Class="UseDropDownMenu.Page"
    .....
    xmlns:mnu="clr-namespace:glMenu"
    ..... >
    <Grid x:Name="LayoutRoot" Background="White">
     <Grid x:Name="menuGrid" Canvas.ZIndex="99" >
      <mnu:DropDownMenu x:Name="myMenu" Click="myMenu_Click" />
     </Grid>
     <Grid x:Name="g">
      .....        user grid here
     </Grid>
    </Grid>
 </UserControl> 

In the DropDownMenu.xaml file, we give the XAML description of the main menu bar (implemented as an horizontal StackPanel). Sub-menus (that are vertical StackPanel) will be dynamically added, as needed (and later on removed) :
 <UserControl x:Class="glMenu.DropDownMenu"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    >
  <Grid x:Name="menuGrid" HorizontalAlignment="Stretch" VerticalAlignment="Top" >
   <StackPanel x:Name="spMainMenu" Loaded="spMainMenu_Loaded"  Orientation="Horizontal"
               Height="30" >
    <StackPanel.Background>
     <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
      <GradientStop Offset="0" Color="#FEF4C1" />
      <GradientStop Offset="1" Color="#F4E186" />
     </LinearGradientBrush>
    </StackPanel.Background>
   </StackPanel>
  </Grid>
 </UserControl>


Thanks to Linq, it's easy to retrieve the Item children (just the children, not the grand-children) of a specific Item (an XElement within the XML file) :
 List<MenuItem> getMenuItems(XElement xml)
 {
  var lstItems = from p in xml.Elements("Item")
                 select new MenuItem
                 {
                  Text = (string)p.Attribute("Text").Value,
                  Id = (int)p.Attribute("Id"),
                  hasSubMenu = p.HasElements,
                  XElem = p
                 };
  return lstItems.ToList();
 }

MenuItem is a class created to hold attributes of a menu item. Menu is another class created to hold attributes of a menu (main menu or sub-menu). Menus displayed at one time are kept in a list of menus :
 List<Menu> lstMenus = new List<Menu>();

The main menu is created at Loaded time while sub-menus are created (and removed) as needed. A menu is first created in memory (each menu item is implemented as a Grid containing a rectangle - to mark MouseOver - and a TextBlock for the Text) :
 void PrepareMenuPresentation(Menu mnu) 
 {
   .....
  int N = mnu.lstMenuItems.Count;
  for (int n = 0; n < N; n++)
  {
   TextBlock txtItem = new TextBlock(); 
   .....    
   Rectangle rcItem = new Rectangle(); 
   ......
   Grid gridItem = new Grid();
   .....  
   gridItem.MouseLeave += new MouseEventHandler(item_MouseLeave);
   gridItem.MouseEnter += new MouseEventHandler(item_MouseEnter);
   gridItem.MouseLeftButtonDown += new MouseButtonEventHandler(item_MouseLeftButtonDown);
   gridItem.MouseMove += new MouseEventHandler(item_MouseMove);
   gridItem.Children.Add(rcItem); gridItem.Children.Add(txtItem);
   mnu.StackPan.Children.Add(gridItem);
  }
   .....   
 } 


The mouse events are handled to control navigation from menu items to menu items (and, remember, sub-menus are dynamically added or removed).

Our code (300 lines) is not that long and complex but it would be too annoying - and useless - to explained it here line by line. Moreover, I tried to document the code as well as I could.

Tags:

Menus. Part I

by gleblanc 12. October 2008 12:54

With this post, we are starting a serie of posts dedicated to menus. In this post, we present the basis with a basic menu, though XML driven and, eventually, linked to a status bar. We'll improve it in the forthcoming posts.


Project / source code

As usual, we create a Silverlight application that will host the menu we are building here. Let's assume the application is named Menu1. Visual Studio creates a namespace for the program and, by default, this namespace is named Menu1, as the application.

Our menu is described in a XML file (Menu.xml) and this XML file is inserted as resource into the Silverlight application (Solution Explorer, Add, New element, XML file) :
 <?xml version="1.0" encoding="utf-8" ?>
 <Menu>
  <Item Id="10" Text="Monet" Help="Claude Monet : Thames at Westminster" />
  <Item Id="20" Text="Turner" Help="Joseph Turner : Oxford High Street" />
  <Item Id="30" Text="Renoir" Help="Auguste Renoir : Pont des arts, Paris" />
  <Item Id="40" Text="Pissaro" Help="Camille Pissaro : L'Hermitage" Selected="True" />
  <Item Id="50" Text="Vermeer" Help="Johannes Vermeer : View of Delft" />
 </Menu>

Each menu item must have an Id and a Text attributes and, eventually, a Help sentence (a tooltip eventually displayed in the status bar) and a Selected attribute (the menu item with a Selected attribute is displayed in red and, at program startup, a request is made to the hosting application to respond to that item, as if the user had clicked on that menu item). This makes that menu item the currently selected menu item.

Let's now create the menu, by adding a Silverlight User Control (Solution Explorer, Add, Add new element, Silverlight User Control), that we name Menu.xaml. By default, it's created in the Menu1 namespace (as the hosting program). Since we intend to create a menu that will be easily used in another hosting application, we prefer a specific namespace (here Menu) for the menu (we'll do the same later on for the status bar). This requires two changes : one in the Menu.xaml file (value of x:Class attribute changed to Menu.Menu instead of Menu1.Menu) and one in the Menu.xaml.cs file (namespace Menu instead of namespace Menu1).

In the Menu.xaml file, we delete the Width and Height attributes and force an horizontal StackPanel as container (instead of a Grid). We alse force a Background color with a gradient (it's generally considered nicer) :
 <UserControl x:Class="Menu.Menu"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    >
  <StackPanel x:Name="spMenu" HorizontalAlignment="Stretch" Height="40"
              Loaded="spMenu_Loaded" Orientation="Horizontal" >
   <StackPanel.Background>
    <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
     <GradientStop Offset="0" Color="#FEF4C1" />
     <GradientStop Offset="1" Color="#F4E186" />
    </LinearGradientBrush>
   </StackPanel.Background>
  </StackPanel>
 </UserControl>

We create a class (named MenuItem) for a menu item (Solution Explorer, Add, New element, Class), again in the Menu namespace (one line to change in the MenuItem.cs file) :
 namespace Menu
 {
  public class MenuItem
  {
   public int Id { get; set; }
   public string Text { get; set; }
   public string Help { get; set; }
   public bool Selected { get; set; }
  }
 }

In the spMenu_Loaded function, we'll open the Menu.xml file and analyze it using the Linq technology. To do so, need to add a reference (Solution Explorer, Add reference, .NET) to System.Xml.Linq. We also need to add using System.Xml.Linq; to the Menu.xaml.cs file. To analyze our XML file and create a list of MenuItem, we write in Menu.xaml.cs (remember, Help and Selected attributes could be absent : easy for Help, thanks to the null value for a string but a check is necessary for Selected - whatever the value, we consider here, for the sake of simplicity, that the item is selected if the Selected attribute is present) :
 List<MenuItem> tabItems;
 .....
 private void spMenu_Loaded(object sender, RoutedEventArgs e)
 {
  XElement xml = XElement.Load("Menu.xml");
  var listItems = from p in xml.Elements("Item")
                  select new MenuItem
                  {
                   Text = (string)p.Attribute("Text").Value,
                   Id = (int)p.Attribute("Id"),
                   Help = (string)p.Attribute("Help"),
                   Selected=p.Attribute("Selected")!=null ? true : false
                  };
  tabItems = listItems.ToList();
  .....
 }

In the menu bar, each menu item is implemented as a single-cell Grid containing a rectangle (for the color change on MouseOver), a TextBlock (for menu item text) and a vertical Line as separator. We also handle mouse events for each menu item :
 private void spMenu_Loaded(object sender, RoutedEventArgs e)
 {
  .....
  int N = tabItems.Count;
  for (int n=0; n<N; n++)
  {
   TextBlock txtItem = new TextBlock();
   txtItem.Text = tabItems[n].Text;
   txtItem.VerticalAlignment = VerticalAlignment.Center;
   txtItem.HorizontalAlignment = HorizontalAlignment.Center;
   switch (tabItems[n].Selected)
   {
    case true : txtItem.Foreground = new SolidColorBrush(Colors.Red); break;
    case false : txtItem.Foreground = new SolidColorBrush(Color.FromArgb(255, 37, 36, 85));
                 break;
   }
   Rectangle rcItem = new Rectangle(); rcItem.Fill = new SolidColorBrush(Colors.Transparent);
   rcItem.Fill = new SolidColorBrush(Colors.Transparent);
   rcItem.Width = txtItem.ActualWidth +6;
   Grid gridItem = new Grid();
   gridItem.Width = txtItem.ActualWidth+55;
   gridItem.Tag = n;

   Line liSep = new Line();
   liSep.X1 = gridItem.ActualWidth-5; liSep.Y1 = 5;
   liSep.X2 = gridItem.ActualWidth-5; liSep.Y2 = 35; liSep.StrokeThickness = 5;
   liSep.Stroke = new SolidColorBrush(Color.FromArgb(255, 255, 245, 190));

   gridItem.MouseLeave += new MouseEventHandler(c_MouseLeave);
   gridItem.MouseEnter += new MouseEventHandler(c_MouseEnter);
   gridItem.MouseLeftButtonDown += new MouseButtonEventHandler(c_MouseLeftButtonDown);

   gridItem.Children.Add(rcItem);
   gridItem.Children.Add(txtItem); gridItem.Children.Add(liSep);
   spMenu.Children.Add(gridItem);
   .....
  }
At the end of the spMenu_Loaded function, we need to warn the hosting application that the Selected menuItem (here the item with Id 40) has to be activated. To do so, we first need a class (here named MenuEventArgs) derived from EventArgs : Solution Explorer, Add, New element, Class, again in the Menu namespace. Visual Studio creates the MenuEventArgs.cs file, that contains (forgetting a few using at the beginning) :
 namespace Menu
 {
  public class MenuEventArgs : EventArgs
  {
   public int ItemId { get; set; }
  }
 }
In the Menu.xaml.cs file, we need to add a event (here named Click) to warn the hosting application that a menu item has been selected (nSelected containing the ordinal position of the menu item selected in the XML file) :
 public delegate void MenuEventHandler(object sender, MenuEventArgs e);
 public event MenuEventHandler Click;
 .....
 private void spMenu_Loaded(object sender, RoutedEventArgs e)
 {
  .....
  if (nItemSelected!=-1)
  {
   MenuEventArgs args = new MenuEventArgs(); args.ItemId = tabItems[nItemSelected].Id;
   if (Click != null) Click(this, args);
  }
 }
In the mouse event functions, we mark the MouseOver selection or inform the hosting application that a menu item was clicked :
 void c_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
 {
  Grid gridItem = sender as Grid;
  int nItem = (int)gridItem.Tag;
  MenuEventArgs args = new MenuEventArgs(); args.ItemId = tabItems[nItem].Id;
  TextBlock txtItem = gridItem.Children[1] as TextBlock;
  txtItem.Foreground = new SolidColorBrush(Colors.Red);
  if (nItemSelected!=-1) 
  {
   Grid gSel = spMenu.Children[nItemSelected] as Grid;
   TextBlock txtSelItem = gSel.Children[1] as TextBlock;
   txtSelItem.Foreground = new SolidColorBrush(Color.FromArgb(255, 37, 36, 85));
  }
  nItemSelected=nItem;
  if (Click != null) Click(this, args); 
  } 

  void c_MouseEnter(object sender, MouseEventArgs e)
  {
   Grid gridItem = sender as Grid;
   Rectangle rcItem = gridItem.Children[0] as Rectangle;
   rcItem.Fill = new SolidColorBrush(Color.FromArgb(255, 203, 203, 239));
   int nItem = (int)gridItem.Tag;
   .....
  }

  void c_MouseLeave(object sender, MouseEventArgs e)
  {
   Grid gridItem = sender as Grid;
   Rectangle rcItem = gridItem.Children[0] as Rectangle;
   rcItem.Fill = new SolidColorBrush(Colors.Transparent);
   .....
  }
Now, let's add the menu in the hosting application. In Page.xaml, we inform Visual Studio that we'll use Menu (one line to add) :
 <UserControl x:Class="Menu1.Page"
   .....
   xmlns:mnu="clr-namespace:Menu"
   .....
   >
mnu is now the prefix we decide to give to reference our menu. The user inserts for instance the menu in a grid row (in Page.xaml)  :
 <mnu:Menu x:Name="myMenu" Grid.Row="0" Click="myMenu_Click" />
And the myMenu_Click function (in Page.xaml.cs) is something like :
 private void myMenu_Click(object sender, MenuEventArgs e)
 {
  ..... // menu item clicked in e.Id
 }
And now the status bar : we create a Silverlight User Control (StatusBar.xaml), again in the Menu namespace (do not forget the namespace change in StatusBar.xaml.cs). The StatusBar.xaml file becomes :
 <UserControl x:Class="Menu.StatusBar"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      >
  <Grid x:Name="LayoutRoot" Background="White">
   <Border Background="#F4E186" >
    <TextBlock x:Name="txtToolTip" FontSize="10" Foreground="Blue"
               VerticalAlignment="Center"  />
   </Border>
  </Grid>
 </UserControl>
To make the link between the menu and the status bar, we add a property in the Menu class :
 public StatusBar SB { get; set; }
The user inserts a status bar (in Page.xaml) :
 <mnu:StatusBar x:Name="myStatusBar" Grid.Row="2" />
and force the link between the menu and the status bar (for instance in the Loaded function in Page.xaml.cs) :
 myMenu.SB = myStatusBar;

Nothing Silverlight-ish in this menu but we are now ready for more Silverlight-like menus. See you in next posts for that. 

Tags:

Turn the page !

by gleblanc 29. September 2008 19:24

!!!!!!! This page has been updated for Silverlight 2 RTM : see post of Nov 18th (http://www.gleblanc.eu/Blog/post/Page-turner-updated-for-Silverlight-2-TRM.aspx)  !!!!! 

In this post, we'll turn pages, thanks to an outstanding component developed and made freely available by Mitsu Furuta of Microsoft France.

Turn the pages using the mouse, as if it was a book ! There are five paintings of Brueghel (a painter of 16th century) to be seen.
 


Project / source code 

To add such a "page turner" into your Silverlight program, download the DLL that does the job. First, access the project on Codeplex. The component was developed by Mitsu Furuta of Microsoft France. Incidentally, Mitsu is blogging in French but also in English.

Download the zip file, unzip it, compile the project in the SLBookDemoApp directory and extract the SLMitsuControls.dll file that is created by the compiler in the ClientBin sub-directory.

The use of this component is under "Microsoft Public License" (MS-PL). I can read a computer book as if it was a novel but reading just a few paragraphs of lawyer's jargon always gives me headache. But here it's rather clear : you can use the component "as it is", even in commercial applications, royalty-free (I mean the component described here is royalty-free, not necessarily your application). Of course, you can't claim rights on this component... For me, it was obvious. Programmers and lawyers seem to have a brain somewhat different. 

In your application, add a reference to the DLL you have just extracted and saved somewhere. In the UserControl tag in the xaml file, add :
 xmlns:local="clr-namespace:SLMitsuControls;assembly=SLMitsuControls"
I chose local for future reference to the component but any name could be chosen.

Now, you need to add a component that has a visual appearence :
 <local:UCBook x:Name="book" ..... />
where ..... can be replaced by any property you usually find in a component (Grid.Row, Canvas.Left, etc.).

Now, let's see the changes that have to be made in your Page.xaml.cs file. Your Page class must implement the IDataProvider interface (and consequently implement GetItem and GetCount functions). Some initialization (one line) has also to be done in the function that handles the Loaded event :
 using SLMitsuControls;
 .....
 public partial class Page : UserControl,IDataProvider
 {
  .....
  private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
  {
   book.SetData(this);
  }
  public object GetItem(int index)
  {
   return pages.Items[index];
  }
  public int GetCount()
  {
   return pages.Items.Count;
  }
  .....
 }

The images (here five) have to be mentionned in Grid.Resources (or Canvas.Resources, depending on the container) :
 <Grid .... >
  <Grid.Resources>
  <ItemsControl x:Name="pages" >
   <Image Source="Brueghel1.jpg" Stretch="UniformToFill"  />
   <Image Source="Brueghel2.jpg" Stretch="UniformToFill"    />
   <Image Source="Brueghel3.jpg" Stretch="UniformToFill" />
   <Image Source="Brueghel4.jpg" Stretch="UniformToFill"   />
   <Image Source="Brueghel5.jpg" Stretch="UniformToFill"  />
  </ItemsControl>
 </Grid.Resources>
 ....
 </Grid>

The images (here 5 images, from Brueghel1.jpg to Brueghel5.jpg) need to be copied in the ClientBin directory (the directory of the xap file). This is often an overlooked step : adding the images in the project (as often done for images) doesn't help. The images need to be explicitly copied into the ClientBin directory. 

To know which pages are visible, just handle the OnPageTurned event :
 <local:UCBook x:Name="book" ..... OnPageTurned="book_OnPageTurned" />
 .....
 private void book_OnPageTurned(int leftPageIndex, int rightPageIndex)
 {
  .....
 }

The leftPageIndex and rightPageIndex arguments get the value -1 when a page is not displayed (for instance there is no page at the left when the book is initially opened).

A page is not restricted to images : it could contain a set of components. Just replace the <Image ..... /> tag by any other tag, for instance a grid tag (container for all the elements of a page).  

Tags:

Silverlight 2 RC0 and the programs presented here

by gleblanc 26. September 2008 22:22

In his blog, Scott Guthrie announces the availability of Silverlight 2 Release Candidate 0.

Please read carefully the terms, still more explicitely stated in Tim Heuer's blog. RC0 is for developers only (well, I guess my readers are), so they can prepare themselves to have their programs ready the day Silverlight 2 comes to the light : there are breaking changes and current Silverlight 2 beta 2 (as my programs currently are) will not run unchanged with Silverlight 2 (RC0 included).

As Tim states with insistance : applications developed using RC0 should not be deployed and there is no RC0 run-time available for download ! In other words, use RC0 just to update and test your own Silverlight 2 programs and only on your development machine.

Though I intend to document changes that will be necessary in my programs (very few, I hope), I am currently sticking to Silverlight 2 Beta 2. I will switch to SL2 RTM and update my programs (included the SL2 programs mentionned in my book) the day Silverlight 2 is officially out.

 Thanks for your comprehension.

Tags:

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen

About the author

Gerard Leblanc is the author of several books (in french) on C++, C#, .NET and Silverlight (Eyrolles, Paris as publisher). See www.gleblanc.eu as the companion web site for these books (included sample programs).
He is Microsoft MVP for Silverlight.

MVP logo