In vielen Windows Phone-Projekten werden Dialoge zur Anzeige von Nachrichten oder zum Abfragen von einfachen Ja-Nein-Fragen benötigt. Oft soll aber ein benutzerdefiniertes Layout statt dem recht puristischen Native-Look von Windows Phone zum Einsatz kommen.

Standard- und nachgebauter MessageDialog Standard- und nachgebauter MessageDialog

Hierzu bieten sich verschiedene Controls diverser Bibliotheken an, wie beispielsweise die CustomMessageBox vom Windows Phone Toolkit oder der CustomDialog vom Callisto-Toolkit. Diese Bibliotheken sind jedoch recht umfangreich, wenn also nur ein eigener Dialog benötigt wird, womöglich etwas zu oversized. Die gleiche Funktionalität für einen einfachen Dialog kann auch recht schnell selbst nachgebaut werden.

Die Implementierung

1. Benutzersteuerelement hinzufügen

Der Einfachheit halber erstellen wir als Basis für den Dialog ein neues Benutzersteuerelement (UserControl). Der Vorteil im Vergleich zu einem CustomControl ist die einfachere Handhabung des Layout und des Codes.

Es wird ein neues Benutzersteuerelement hinzugefügt Es wird ein neues Benutzersteuerelement hinzugefügt

2. Layout für den Dialog

Ein Benutzersteuerelement besteht aus einer XAML-Datei für das Layout und einer Code-Behind-Datei. Folgendes XAML verwenden wir als Basis für den Info-Dialog:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<UserControl
    x:Class="Dialogs.InfoDialog"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Dialogs"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="800"
    d:DesignWidth="480">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="32"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Border Background="SteelBlue"/>

        <Border Grid.Row="1" Background="SteelBlue">
            <StackPanel Margin="20,0">
                <TextBlock x:Name="TitleTextBlock" Text="Der Titel~" Style="{ThemeResource MessageDialogTitleStyle}" />
                <TextBlock x:Name="ContentTextBlock" Text="Der Inhalt~" Margin="0,10,0,0" Style="{ThemeResource MessageDialogContentStyle}" />

                <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                    <Button Content="OK" Margin="10" Tapped="OKButton_Tapped" />
                    <Button Content="Abbrechen" Margin="10" Tapped="CancelButton_Tapped" />
                </StackPanel>
            </StackPanel>
        </Border>

        <Border Grid.Row="2" Background="Black" Opacity="0.65"/>
    </Grid>
</UserControl>

Der Dialog besteht aus einem Grid mit 3 Zeilen. Die erste Zeile mit einer Höhe von 32 stellt sicher, dass kein Inhalt des Dialogs über der System-Titelzeile liegt. Die zweite Zeile ist der eigentliche Inhalt des Dialogs. In dem Beispiel besteht der Dialog aus zwei Textblöcken mit Titel und Inhalt. Ansonsten gibt es noch zwei Buttons zum Bestätigen oder Abbrechen. Die letzte Zeile füllt den Rest des Bildschirms mit einer halbtransparenten Überlagerung, damit der Dialog bildschirmfüllend und komplett im Vordergrund angezeigt werden kann.

Vorschau des Info Dialogs im Designer Vorschau des Info Dialogs im Designer

3. Code für den Dialog

In der Code-Behind-Datei wird nun die Logik für den Dialog hinterlegt. Diese besteht aus folgenden Teilen (der vollständige Code befindet sich am Ende des Artikels).

1
2
3
4
5
6
7
8
9
10
11
12
13
// Popup control for displaying the dialog
private Popup popup;

// For async open/close operation
private TaskCompletionSource<bool> taskCompletionSource;

public InfoDialog(string content, string title)
{
    this.InitializeComponent();

    this.TitleTextBlock.Text = title;
    this.ContentTextBlock.Text = content;
}

Der Dialog soll modal angezeigt werden, das System soll also mit der Ausführung des weiteren Codes warten, bis der Dialog wieder geschlossen wird. Hierzu verwenden wir ein TaskCompletionSource. Dieses ermöglicht uns einen Task zu erstellen, der so lange läuft, bis dessen Ergebnis explizit gesetzt wird. Dadurch können wir den Dialog solange offen halten, bis der Benutzer diesen durch Knopfdruck wieder schließt. Das Popup wird später dazu verwendet, um den Dialog anzuzeigen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <summary>
/// Opens the dialog.
/// </summary>
public Task<bool> ShowAsync()
{
    this.taskCompletionSource = new TaskCompletionSource<bool>();
    this.CreateDialog();

    return this.taskCompletionSource.Task;
}

// Create a popup control and set content
private void CreateDialog()
{
    this.Height = Window.Current.Bounds.Height;
    this.Width = Window.Current.Bounds.Width;

    this.popup = new Popup();
    this.popup.Child = this;

    HardwareButtons.BackPressed += HardwareButtons_BackPressed;

    this.popup.IsOpen = true;
}

Zum Anzeigen des Dialogs verwenden wir die Methode ShowAsync(), die ein Task zurückgibt, somit asynchron ausgeführt wird. Hier wird ebenfalls das TaskCompletionSource instanziiert und dessen Task zurückgegeben. Der Task wird bei Aufruf der Methode aktiv und wird erst beendet, wenn dessen Ergebnis über taskCompletionSource.SetResult gesetzt wird.

In CreateDialog wird die Höhe und Breite des Dialogs auf die des Geräts gesetzt, um den gesamten Bildschirm auszufüllen. Danach wird das Popup angelegt und dessen Inhalt auf das aktuelle Benutzersteuerelement gesetzt. Da die Hardware-Taste für Zurück ebenfalls behandelt werden sollte, wird das Event zugewiesen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Event handler for back button
private void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
    HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
    if (this.popup.IsOpen)
    {
        this.Close(false);
        e.Handled = true;
    }
}

private void OKButton_Tapped(object sender, TappedRoutedEventArgs e)
{
    this.Close(true);
}

private void CancelButton_Tapped(object sender, TappedRoutedEventArgs e)
{
    this.Close(false);
}

Wird die Zurück-Taste betätigt, wird der Dialog geschlossen und das Ergebnis auf false gesetzt, was einen Abbruch signalisieren soll. Die beiden Buttons im Dialog signalisieren der Close-Methode ebenfalls mit den dementsprechenden Flags (true/false) das Ergebnis des Dialogs.

1
2
3
4
5
6
7
8
9
10
11
// Closes the dialog
private void Close(bool success)
{
    HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
    this.popup.IsOpen = false;

    if (this.taskCompletionSource != null)
    {
        this.taskCompletionSource.SetResult(success);
    }
}

In der Close-Methode wird das Popup dann geschlossen und der Task mit dem jeweiligen Ergebnis beendet.

4. Dialog aus der App aufrufen

Der Dialog kann nun wie auch der herkömmliche MessageDialog aus der App heraus aufgerufen werden:

1
2
3
4
5
private async void Button_Click(object sender, RoutedEventArgs e)
{
    var dialog = new MessageDialog("Hier steht die Nachricht", "Titel");
    var result = await dialog.ShowAsync();
}

Über result kann der Ausgang des Dialogs abgefragt werden (boolescher Wert).

Der fertige Dialog Der fertige Dialog

Der komplette Dialog

Hier folgt nochmal der gesamte Code des Dialogs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public sealed partial class InfoDialog : UserControl
{
    // Popup control for displaying the dialog
    private Popup popup;

    // For async open/close operation
    private TaskCompletionSource<bool> taskCompletionSource;

    public InfoDialog(string content, string title)
    {
        this.InitializeComponent();

        this.TitleTextBlock.Text = title;
        this.ContentTextBlock.Text = content;
    }

    /// <summary>
    /// Opens the dialog.
    /// </summary>
    public Task<bool> ShowAsync()
    {
        this.taskCompletionSource = new TaskCompletionSource<bool>();
        this.CreateDialog();

        return this.taskCompletionSource.Task;
    }

    // Create a popup control and set content
    private void CreateDialog()
    {
        this.Height = Window.Current.Bounds.Height;
        this.Width = Window.Current.Bounds.Width;

        this.popup = new Popup();
        this.popup.Child = this;

        HardwareButtons.BackPressed += HardwareButtons_BackPressed;

        this.popup.IsOpen = true;
    }

    // Event handler for back button
    private void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
    {
        HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
        if (this.popup.IsOpen)
        {
            this.Close(false);
            e.Handled = true;
        }
    }

    private void OKButton_Tapped(object sender, TappedRoutedEventArgs e)
    {
        this.Close(true);
    }

    private void CancelButton_Tapped(object sender, TappedRoutedEventArgs e)
    {
        this.Close(false);
    }

    // Closes the dialog
    private void Close(bool success)
    {
        HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
        this.popup.IsOpen = false;

        if (this.taskCompletionSource != null)
        {
            this.taskCompletionSource.SetResult(success);
        }
    }
}

Bonus: Über wenige Anpassungen zum InputDialog

Durch den einfachen Aufbau über ein Benutzersteuerelement, kann der oben gezeigte InfoDialog schnell zu einem Dialog mit Eingabefeld gemacht werden. Hierzu wird die XAML-Datei um eine TextBox erweitert:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<UserControl
    x:Class="Dialogs.InputDialog"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:Dialogs"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="800"
    d:DesignWidth="480">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="32"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Border Background="SteelBlue"/>

        <Border Grid.Row="1" Background="SteelBlue">
            <StackPanel Margin="20,0">
                <TextBlock x:Name="TitleTextBlock" Text="Der Titel~" Style="{ThemeResource MessageDialogTitleStyle}" />
                <TextBlock x:Name="ContentTextBlock" Text="Der Inhalt~" Margin="0,10,0,0" Style="{ThemeResource MessageDialogContentStyle}" />
                <TextBox x:Name="InputTextBox" Margin="0,10,0,0" />

                <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
                    <Button Content="OK" Margin="10" Tapped="OKButton_Tapped" />
                    <Button Content="Abbrechen" Margin="10" Tapped="CancelButton_Tapped" />
                </StackPanel>
            </StackPanel>
        </Border>

        <Border Grid.Row="2" Background="Black" Opacity="0.65"/>
    </Grid>
</UserControl>

Im Code des Dialogs müssen lediglich Anpassungen für den Rückgabetyp des Dialogs gemacht werden. Die TaskCompletionSource wird dementsprechend vom Typ bool in string geändert.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public sealed partial class InputDialog : UserControl
{
    // Popup control for displaying the dialog
    private Popup popup;

    // For async open/close operation
    private TaskCompletionSource<string> taskCompletionSource;

    public InputDialog(string content, string title)
    {
        this.InitializeComponent();

        this.TitleTextBlock.Text = title;
        this.ContentTextBlock.Text = content;
    }

    /// <summary>
    /// Opens the dialog.
    /// </summary>
    public Task<string> ShowAsync()
    {
        this.taskCompletionSource = new TaskCompletionSource<string>();
        this.CreateDialog();

        return this.taskCompletionSource.Task;
    }

    // Create a popup control and set content
    private void CreateDialog()
    {
        this.Height = Window.Current.Bounds.Height;
        this.Width = Window.Current.Bounds.Width;

        this.popup = new Popup();
        this.popup.Child = this;

        HardwareButtons.BackPressed += HardwareButtons_BackPressed;

        this.popup.IsOpen = true;
    }

    // Event handler for back button
    private void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
    {
        HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
        if (this.popup.IsOpen)
        {
            this.Close(false);
            e.Handled = true;
        }
    }

    // Closes the dialog
    private void Close(bool success)
    {
        HardwareButtons.BackPressed -= HardwareButtons_BackPressed;
        this.popup.IsOpen = false;

        if (this.taskCompletionSource != null)
        {
            if (success)
            {
                this.taskCompletionSource.SetResult(this.InputTextBox.Text);
            }
            else
            {
                this.taskCompletionSource.SetResult(null);
            }
        }
    }

    private void OKButton_Tapped(object sender, TappedRoutedEventArgs e)
    {
        this.Close(true);
    }

    private void CancelButton_Tapped(object sender, TappedRoutedEventArgs e)
    {
        this.Close(false);
    }
}

Das Ergebnis sieht dann so aus:

Der fertige InputDialog Der fertige InputDialog

Beispielprojekt

Das Beispielprojekt ist hier auf GitHub zu finden.