May 2008 - Posts
Apparently Radio 1's Big Weekend is a big thing in England, and to launch the festivities they've made The Big Zoomy Photo Thing a central piece of the action (as you may guess, this is Silverlight/Zoom incognito).
(picture cropped to save room)
The implementation is beautiful. Click, roll, zoom. To give you a quick sense of it, here are two images. The first is the starter, I then zoomed in on the control panel (pointed to by the red arrows I added.
The progressive rendering is just mind-blowing. Every time I see it. And it is a blast to see it in a live usage, even if they don't have any really interesting pictures up yet (they will soon).
One of the folks who did a lot of the programming is a buddy (he refuses to be credited) and I will work on talking him into an interview about the programming experience. Stay tuned. I know that Tim Heuer is also taking a long look at Zoom so we'll have a good deal more on this very soon.
Aha! Okay, I may not fully understand all the requirements, but the following demo will show how to dynamically create a user control and then have that user control close itself, remove itself from the containing page and fire an event to the page so that the page can clean up any associated other controls that might be left laying about.
This is based on the User Control sample that goes with the video that hasn't yet been posted (you don't mind that, do you?) but will be in a couple days. I'll strip it down so as not to get hung up in the parts we don't care about.
First, let's look at the effects. When the application begins there is just a single button marked "Create".
Clicking on that button creates two text blocks and two user controls,
In the UserControl video the User Controls are quite nicer looking but here we're interested in their ability to self-destruct; hence the close button.
When you click the close button, not only does the User Control remove itself from its parent panel's children collection, it raises an event to which the page can subscribe so that it can clean up anything else that might be lingering about; in this case the text block (which is not part of the control). Thus, if I close the upper control, I want also to remove the "Event Address" prompt.
Here's how it all works. I assume we have the custom control already and I add the button to it. The key is to give that user control its own EventArgs type (to hold its unique ID ) and thus also give it a delegate and an event.
public partial class AddressUserControl : UserControl
{
public class AddressEventArgs : RoutedEventArgs
{
public object Tag { get; private set; }
public AddressEventArgs(object theTag)
{
this.Tag = theTag;
}
}Notice both that AddressEventArgs is derived from RoutedEventArgs adn that it is nested within AddressUserControl (my User Control). It has a constructor and a public property called Tag (to parallel the idea that the control itself has a Tag of type object).
We now give the control a delegate and an event
public delegate void AddressEventHandler(object o, AddressEventArgs e);
public event AddressEventHandler Closed;
The closed event is what the page will subscribe to, in order to be alerted when the control is closed. This event is fired as part of the control's handling of the button's click event,
public AddressUserControl()
{
InitializeComponent();
Close.Click += new RoutedEventHandler(Close_Click);
}
void Close_Click(object sender, RoutedEventArgs e)
{
Panel parent = this.Parent as Panel;
if (parent != null)
{
parent.Children.Remove(this);
if (Closed != null && this.Tag != null)
{
Closed(this, new AddressEventArgs(this.Tag));
}
}
}
In the constructor we wire up the Close.Click; our internal handler for when the button is pressed. That handler does two things; it first makes sure we're in a panel, and if so, it removes us from the panel. It then checks to see if anyone has registered with our Closed event and that our Tag is not null; if so then it fires the Closed event to anyone who is interested.
Creating The Control Dynamically
All of the above falls into place when you see the control created dynamically. The XAML has nothing but the stack panel to hold the dynamically created controls,
Page.xaml
<UserControl x:Class="UserControlDemo.Page"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:jl="clr-namespace:UserControlDemo;assembly=UserControlDemo"
Width="600" Height="800">
<StackPanel x:Name="MasterContainer" Background="White">
<Button x:Name="Create" Content="Create" Width="60" Height="40" HorizontalAlignment="Left"/>
</StackPanel>
</UserControl>Here's how the control is created in Page.xaml.cs:
void Create_Click(object sender, RoutedEventArgs e)
{
TextBlock tb = new TextBlock();
tb.Text = "Event Address";
tb.FontFamily = new FontFamily("Verdana");
tb.FontSize = 24;
tb.HorizontalAlignment = HorizontalAlignment.Left;
tb.Margin = new Thickness(15, 0, 0, 0);
tb.Tag = "1";
MasterContainer.Children.Add(tb);
AddressUserControl auc = new AddressUserControl();
auc.Tag = "1";
auc.Closed += new AddressUserControl.AddressEventHandler(auc_Closed);
MasterContainer.Children.Add(auc);
tb = new TextBlock();
tb.Text = "Billing Address";
tb.FontFamily = new FontFamily("Verdana");
tb.FontSize = 24;
tb.HorizontalAlignment = HorizontalAlignment.Left;
tb.Margin = new Thickness(15, 0, 0, 0);
tb.Tag = "2";
MasterContainer.Children.Add(tb);
auc = new AddressUserControl();
auc.Tag = "2";
auc.Closed += new AddressUserControl.AddressEventHandler(auc_Closed);
MasterContainer.Children.Add(auc);
}Unpacking this, we start by dynamically creating a textblock and adding it to the stack panel. We then create an AddressUserControl and assign it the same tag as the TextBlock and then we register with the user control's closed event, passing in the name of the method to be invoked when that event is raised (auc_closed). Finally, we add the user control to the stack panel.
This is repeated for the second text block and the second user control.
When the user clicks on the button, the user control takes care of removing itself from the stack panel, but it also fires the Closed event, which the page has now registered for. Per the registration, the method auc_Closed is called,
void auc_Closed(object o, AddressUserControl.AddressEventArgs e)
{
foreach (UIElement uie in MasterContainer.Children)
{
TextBlock tb = uie as TextBlock;
if (tb != null)
{
if (tb.Tag.ToString().Equals(e.Tag.ToString()))
{
MasterContainer.Children.Remove(uie);
break;
}
}
}
}
Auc_Closed iterates through the stack panel's children collection looking for textBlocks. If it finds one it checks the Tag against the tag in the AddressEventArgs (put there when the event was fired) and if they match, then it removes that text block from the stack panel as well.
Sweet.
I've put the entire source code Here
Ben H. asked a question within a comment to a previous blog post.
Suppose, there is a button on the main form and when the user clicks on the button, a new container gets instantiated and the new container appears on the top of the main form. However, there is also a "Close" button on that container that when the user clicks on, the container closes. And here is my question. When the user clicks on the the "Close" button, I'll set the visibility of that Container to Collapsed, however I need to raise some kind of event in that Close button that the main form (who originally created the Container), will respond to this event that the container has been closed, therefore I need to remove it from the Children.Remove(Container) and then set the container to null.
I don't know how to raise that event in the "Close" button and how to respond to it in the main form.
A sample or explanation or snippet or pointer to some info is much appreciated!
I fully admit that I may not have answered this completely, but after a few minutes of noodling with it, here is what I have... which may be a good start or may be off point but perhapsinteresting anyway.
I created a page.xaml with a grid (one row, two columns) and a button,
<UserControl x:Class="DynamicControls.Page"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button x:Name="Create" Content="Create"
Width="50" Height="30" Grid.Row="0"
Grid.Column="0" />
</Grid>
</UserControl>
In the code behind I created an event handler for the button,
private int id = 0;
public Page()
{
InitializeComponent();
Loaded += new RoutedEventHandler(Page_Loaded);
}
void Page_Loaded(object sender, RoutedEventArgs e)
{
Create.Click +=new RoutedEventHandler(Create_Click);
}
The job of the event handler is to dynamically create a container that holds two buttons, one of which is the close button.
void Create_Click(object sender, RoutedEventArgs e)
{
StackPanel sp = new StackPanel();
sp.Orientation = Orientation.Horizontal;
sp.VerticalAlignment = VerticalAlignment.Center;
sp.Width = 300;
sp.Height = 500;
sp.SetValue(Grid.RowProperty, 0);
sp.SetValue(Grid.ColumnProperty, 1);
sp.Tag = (++id).ToString();
sp.Background = new SolidColorBrush(Colors.Cyan);
Button btn = new Button();
btn.Width = 50;
btn.Height = 30;
btn.Content = "Close";
btn.Margin = new Thickness(5.0, 0, 0, 0);
btn.Tag = id.ToString();
btn.Click += new RoutedEventHandler(btn_Click);
sp.Children.Add(btn);
btn = new Button();
btn.Width = 50;
btn.Height = 30;
btn.Content = "Hello";
btn.Margin = new Thickness(5.0, 0, 0, 0);
btn.Click += new RoutedEventHandler(otherBtn_click);
sp.Children.Add(btn);
LayoutRoot.Children.Add(sp);
}
Note that the stack panel is assigned a unique ID in its TAG property as is the button
We'll come back to what this is for in just a moment. After adding the stack panel we add two buttons. One to close the stack panel, and another just to have something else in the stack panel (in this case a button that knows how to change its background color.
Note that each of the buttons is added to the stack panel's children collection and the stack panel itself is added to the grid's children collection.
sp.Children.Add(btn);
LayoutRoot.Children.Add(sp);
Events
Each of the buttons has its own event, and its own event handler. The second button has an event handler cleverly named "otherBtn_click" (it was late, I was tired...)
btn.Click += new RoutedEventHandler(otherBtn_click);
That event handler picks one of six colors and sets the background for the button,
void otherBtn_click(object sender, RoutedEventArgs e)
{
int randomNumber;
Random r = new Random();
randomNumber = r.Next(0,5);
List<Color> myColors = new List<Color>();
myColors.Add(Colors.Magenta);
myColors.Add(Colors.Purple);
myColors.Add(Colors.Red);
myColors.Add(Colors.Gray);
myColors.Add(Colors.Green);
myColors.Add(Colors.Blue);
Button btn = sender as Button;
if ( btn != null )
{
btn.Background = new SolidColorBrush(myColors[randomNumber]);
btn.Foreground = new SolidColorBrush(Colors.Black);
}
}
Close The Door!
The first button is the one that answers the question. Its job is to close the form.
To be explicit, when the user clicks on the Close button, we want the page to be alerted. To accomplish this, we assign an event to the button
btn.Click += new RoutedEventHandler(btn_Click);
The event handler looks through all the elements in the children of the page to find any of type stack panel. If it finds a stack panel it looks to see if the stack panel's Tag holds an ID that matches the ID of the button that fired the event. If so, it has the right stack panel and it removes it from the page's children collection and <poof> it's gone.
void btn_Click(object sender, RoutedEventArgs e)
{
foreach (UIElement uie in LayoutRoot.Children)
{
StackPanel sp = uie as StackPanel;
Button btn = sender as Button;
if (sp != null && btn != null)
{
if (sp.Tag.ToString().Equals(btn.Tag.ToString()))
{
LayoutRoot.Children.Remove(uie);
break;
}
}
} // end foreach
} // end btn_Click
There may be easier ways, but this works quite cleanly. As an exercise for the reader I suggest implementing this so that each click of the Create button creates a stack panel in a different grid location, so that you can have a few open and see that each close button closes the correct one.
Here's the complete source code
Page.xaml
<UserControl x:Class="DynamicControls.Page"
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150" />
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button x:Name="Create" Content="Create" Width="50" Height="30" Grid.Row="0" Grid.Column="0" />
</Grid>
</UserControl>
Page.xaml.cs
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using System.Collections.Generic;
namespace DynamicControls
{
public partial class Page : UserControl
{
private int id = 0;
public Page()
{
InitializeComponent();
Loaded += new RoutedEventHandler(Page_Loaded);
}
void Page_Loaded(object sender, RoutedEventArgs e)
{
Create.Click +=new RoutedEventHandler(Create_Click);
}
void Create_Click(object sender, RoutedEventArgs e)
{
StackPanel sp = new StackPanel();
sp.Orientation = Orientation.Horizontal;
sp.VerticalAlignment = VerticalAlignment.Center;
sp.Width = 300;
sp.Height = 500;
sp.SetValue(Grid.RowProperty, 0);
sp.SetValue(Grid.ColumnProperty, 1);
sp.Tag = (++id).ToString();
sp.Background = new SolidColorBrush(Colors.Cyan);
Button btn = new Button();
btn.Width = 50;
btn.Height = 30;
btn.Content = "Close";
btn.Margin = new Thickness(5.0, 0, 0, 0);
btn.Tag = id.ToString();
btn.Click += new RoutedEventHandler(btn_Click);
sp.Children.Add(btn);
btn = new Button();
btn.Width = 50;
btn.Height = 30;
btn.Content = "Hello";
btn.Margin = new Thickness(5.0, 0, 0, 0);
btn.Click += new RoutedEventHandler(otherBtn_click);
sp.Children.Add(btn);
LayoutRoot.Children.Add(sp);
}
void btn_Click(object sender, RoutedEventArgs e)
{
foreach (UIElement uie in LayoutRoot.Children)
{
StackPanel sp = uie as StackPanel;
Button btn = sender as Button;
if (sp != null && btn != null)
{
if (sp.Tag.ToString().Equals(btn.Tag.ToString()))
{
LayoutRoot.Children.Remove(uie);
break;
}
}
} // end foreach
} // end btn_Click
void otherBtn_click(object sender, RoutedEventArgs e)
{
int randomNumber;
Random r = new Random();
randomNumber = r.Next(0,5);
List<Color> myColors = new List<Color>();
myColors.Add(Colors.Magenta);
myColors.Add(Colors.Purple);
myColors.Add(Colors.Red);
myColors.Add(Colors.Gray);
myColors.Add(Colors.Green);
myColors.Add(Colors.Blue);
Button btn = sender as Button;
if ( btn != null )
{
btn.Background = new SolidColorBrush(myColors[randomNumber]);
btn.Foreground = new SolidColorBrush(Colors.Black);
}
}
} // end class
}
In what may be a first (at least for me) I've secured agreement with the very generous folks I work for here and at O'Reilly to loosely join the tutorials and the chapters of our forthcoming book Programming Silverlight (co-authored by Tim Heuer, O'Reilly 2008) to create an enhanced approach to learning tool.
Let me be clear: the goal is to provide more for less; it is not the goal to push you to buy the book; and you have my word that the tutorials will not be limited, constrained or curtailed in service to making the book somehow more worthwhile.
My hope is that the book will have additional value, but the tutorials, like the videos will stand on their own. If we do it right, together they will supplement one another.
Here's how it will work.
Phase 1 - Tutorials and Draft Chapters
During Phase 1 I will continue to publish tutorials on Silverlight.NET as well as the first drafts of chapters from Programming Silverlight for those who are curious or who would like to provide feedback (more on providing feedback to come).
Note that these chapters will not have gone through technical edit nor copyedit, and will be quite rough; the final chapters will be updated for Silverlight RTW (Release To Web, as opposed to Beta), numerous rounds of technical edit, development edit and copyedit.
Phase 2 - Publication and Beyond
In Phase 2, the 1st Edition of the book will be published, ideally providing cohesion, depth and detail that is simply not possible in the tutorials. It is my plan that the tutorials will continue, supplementing the material already on line, drawing on and extending the book as Silverlight evolves, and serving as both a supplement to the existing edition and a preview of future editions.
Schedule
It is important to understand that the writing schedule for this book does not in any way reflect my knowledge of the timing on the release of Silverlight Beta 2 nor subsequent releases of Silverlight. Further, the current schedule is subject to change and will change. It always does. The Table of Contents will change as well. I'll post both within a week or so.
In my new video I demonstrate how you can create controls dynamically at run time. The technique is very straight forward, everything you can create declaratively in XAML you can also create dynamically in C#.
xaml
<Button x:Name="Button1" Width="50" Height="30" Content="Click Me" Grid.Row="3" Grid.Column="0" />
C#
Button button2 = new Button();
button2.Width = 75;
button2.Height = 30;
button2.Content = "No, click me!";
button2.SetValue(Grid.RowProperty, 0);
button2.SetValue(Grid.ColumnProperty, 1);
LayoutRoot.Children.Add(button2);
Why Not Dynamically Instantiate All of 'Em?
The case for why you must dynamically instantiate some objects is clear: there are times you just can't know at design time what kind of object you'll need: you must respond to the user's choices and actions. That of course raises the question of why not eschew the XAML altogether and dynamically instantiate all your objects.
There are two answers to this. The first is that it is faster and easier to declare the button in XAML. The second, more important answer is that the XAML is "toolable" -- that is, the XAML can be read and understood by, for example, Expression Blend, while the C# cannot; which makes developing large applications far easier and far more scalable.
If I open the project that created these two buttons in Blend, this is what I see:
Blend can read the grid, and the button declared in XAML but is totally unaware of the button created in code.
Conclusion?
The short conclusion is to create objects declaratively at design time when possible, and dynamically at runtime when necessary. In the abstract this can seem confusing, but when writing code, it is never ambiguous.
In about a week, I'll be posting a video on creating a form that responds to keyboard shortcuts (as an intro to a video on user controls).
This is also covered in the tutorial User Controls)
As a challenge to those of you who are already comfortable with styles and data-binding and who "don't need no stinkin' video" -- I've posted a broken version of the program for you to debug. <chortle>
Your mission, should you decide to accept it, is to find the bug, and fix it.
In the video I show this broken version and why it is broken, and then I fix it; but if you'd like to get ahead of the curve and try your hand... have at it. It's a bit sneaky.
What is supposed to happen when you run the app is that if you hit Control-C the address of the Computer Museum is filled in and if you hit Control-M the address of Microsoft is filled in. But it don't. <cackle>