-
dotnetgeek posted this
On a recent project I ran into the need to show a modal dialog box to a user without displaying a second window. For web based applications there are many libraries that provide methods of doing this, jQuery, ASP.NET AJAX, or boxy to name a few. I am going to refer to these “dialogs” as shadow boxes. These shadow boxes are simply user controls that sit on top of the other controls. Please note the use of the word control. It is NOT a new window, the shadow box is simply a control (in this case a WPF user control) layered over the top of the other controls. Since this is going to be a long post lets jump right into some code.
All of the shadown box user controls will implement the following interface. This allows for the containing window to know when to hide the dialog.
public interface IShadowboxDialog { event EventHandler Close; }
Next we need a user control to use for the shadow box.
<UserControl x:Class="ShadowBox.LoginDialog" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300"> <Grid Margin="5"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition /> </Grid.RowDefinitions> <StackPanel> <TextBlock Text="Username" /> <TextBox /> <TextBlock Text="Password" /> <TextBox /> </StackPanel> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Button Grid.Column="1" Content="_OK" IsDefault="True" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="75" Margin="0, 10, 10, 0" Click="okClick" /> <Button Grid.Column="2" Content="_Cancel" IsCancel="True" HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="75" Click="cancelClick" /> </Grid> </Grid> </UserControl>
Here is the code behind which includes the implementation of IShadowboxDialog.
public partial class LoginDialog : UserControl, IShadowboxDialog { public event EventHandler Close; public LoginDialog() { InitializeComponent(); } private void okClick(object sender, System.Windows.RoutedEventArgs e) { invokeClose(); } private void cancelClick(object sender, System.Windows.RoutedEventArgs e) { invokeClose(); } private void invokeClose() { if (Close != null) Close(this, EventArgs.Empty); } }
OK simple enough so far, we have our use control that we want to display as a shadow box, now for the meat and potatoes.
1: <Window x:Class="ShadowBox.Window1"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: xmlns:local="clr-namespace:ShadowBox"
5: Title="MainWindow" Height="350" Width="525">
6: <Grid>
7: <Grid Name="_grdMain">
8: <Grid.ColumnDefinitions>
9: <ColumnDefinition />
10: </Grid.ColumnDefinitions>
11: <Grid.RowDefinitions>
12: <RowDefinition />
13: </Grid.RowDefinitions>
14: <Button Content="Show Dialog" HorizontalAlignment="Left" VerticalAlignment="Center" Click="showClick" />
15: </Grid>
16: <Grid Name="_grdDialogContainer" Visibility="Collapsed">
17: <Grid.Resources>
18: <local:StringNotNullOrWhitespaceVisibilityConverter x:Key="StringNotNullOrWhitespaceVisibilityConverter" />
19: </Grid.Resources>
20: <Grid.Background>
21: <SolidColorBrush Color="{x:Static SystemColors.ControlDarkColor}" Opacity="0.3"/>
22: </Grid.Background>
23: <Grid.ColumnDefinitions>
24: <ColumnDefinition />
25: <ColumnDefinition Width="Auto"/>
26: <ColumnDefinition/>
27: </Grid.ColumnDefinitions>
28: <Grid.RowDefinitions>
29: <RowDefinition />
30: <RowDefinition Height="Auto"/>
31: <RowDefinition />
32: </Grid.RowDefinitions>
33: <Border Grid.Row="1" Grid.Column="1" Background="{x:Static SystemColors.WindowBrush}"
34: BorderBrush="{x:Static SystemColors.WindowFrameBrush}" BorderThickness="1">
35: <StackPanel>
36: <TextBlock Background="{x:Static SystemColors.ActiveCaptionBrush}"
37: Foreground="{x:Static SystemColors.ActiveCaptionTextBrush}"
38: Name="_txtDialogTitle" Padding="4, 2, 2, 2"
39: Visibility="{Binding Path=Text, Mode=OneWay,
40: Converter={StaticResource StringNotNullOrWhitespaceVisibilityConverter}, RelativeSource={RelativeSource Self}}"/>
41: <Grid Name="_grdDialog" MaxWidth="{Binding Width, ElementName=_grdDialogContainer}">
42:
43: </Grid>
44: </StackPanel>
45: </Border>
46: </Grid>
47: </Grid>
48: </Window>
And the code behind.
1: public partial class Window1 : Window
2: {
3: public Window1()
4: {
5: InitializeComponent();
6: }
7:
8: private void showClick(object sender, RoutedEventArgs e)
9: {
10: showDialog(new LoginDialog(), "Login Title");
11: }
12:
13: private void showDialog(IShadowboxDialog argDialog, string argTitle = null)
14: {
15: _grdMain.IsEnabled = false;
16: _grdDialogContainer.Visibility = Visibility.Visible;
17: _grdDialog.Children.Clear();
18: _grdDialog.Children.Add((UIElement)argDialog);
19:
20: _txtDialogTitle.Text = argTitle;
21:
22: EventHandler closedHandler = null;
23: closedHandler = (sender, e) =>
24: {
25: _grdMain.IsEnabled = true;
26: _grdDialogContainer.Visibility = Visibility.Collapsed;
27: _grdDialog.Children.Clear();
28:
29: argDialog.Close -= closedHandler;
30: };
31: argDialog.Close += closedHandler;
32: }
33: }
Just for completeness here is the code for the value converter used above.
public class StringNotNullOrWhitespaceVisibilityConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { return value == null || string.IsNullOrWhiteSpace(value.ToString()) ? Visibility.Collapsed : Visibility.Visible; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } }
So referring back to the XAML above, the basic idea is that the main content grid (Line 6) contains two grids (Lines 7 and 16). Both of these child grids fill the entire content grid. The first grid (Line 7) contains the “main” controls for the window. In this case it is just a single button (Line 14) that displays the shadow box.
The second grid (Line 16) is a bit more interesting. The background brush and opacity are set (Line 21) to give the appearance that the shadow box is above all of the main controls. The shadow box is added to center of the grid, with a boarder around it (Line 33), and an optional header (Line 36).
Now lets look at the code behind, specifically the showDialog method (Line 13). When a shadow box is added to the second grid, it is displayed over the top of the main grid, and the main grid is disabled (Line 15). A self-unregistering annoymous method is registered on the Close event so that when the shadow box is closed the second grid can be hidden and the main grid re-enabled.
The final result looks something like this:

Subsequent posts we will build on this initial design to improve appearance, size handling, re-sizing, and moving the dialog.