.NET Zone is brought to you in partnership with:

Den is a DZone Zone Leader and has posted 460 posts at DZone. You can read more from them at their website. View Full User Profile

Creating a Custom ToastPrompt Control for Windows Phone

04.19.2013
| 2500 views |
  • submit to reddit

Out of the box, the Windows Phone SDK offers developers the capability to request user input with Guide.BeginShowKeyboardInput. This approach, however, can be quite limiting, depending on the situation, especially when the user does not necessarily needs to enter an item but rather select it.

That’s why I created the ToastPrompt control, a simple customizable message dialog, that can be used in tandem with the Windows Phone Toolkit to create a flexible input dialog that slides down from the top of the page and supports both text input, and selection input from a ListPicker.

wp_ss_20121230_0004 wp_ss_20121230_0005

First of all, create a new UserControl in your Windows Phone project. Create a root StackPanel element, that will be the content carrier.

<StackPanel x:Name="stkCustomSet" VerticalAlignment="Top" Margin="0,-260,0,0" Height="230" Background="#ab5800">

    <StackPanel.RenderTransform>

        <TranslateTransform x:Name="stkCustomSetTranslateTransform"></TranslateTransform>

    </StackPanel.RenderTransform>


    <TextBlock Margin="12" Text="{Binding ElementName=CoreToastPrompt, Path=PromptText}" 

Style="{StaticResource PhoneTextLargeStyle}"></TextBlock>

    
    <Grid>

        <TextBox 

            Text="{Binding ElementName=CoreToastPrompt,Path=PromptResult}"

            Visibility="{Binding ElementName=CoreToastPrompt,Path=PromptType, 

Converter={StaticResource TypeToVisibilityConverter}, ConverterParameter=text}" 

            x:Name="txtPromptText"></TextBox>


        <toolkit:ListPicker 

            Visibility="{Binding ElementName=CoreToastPrompt,Path=PromptType,

Converter={StaticResource TypeToVisibilityConverter}}" 

            ItemsSource="{Binding ElementName=CoreToastPrompt,

                    Path=ItemsSource}" 

            ExpansionMode="FullScreenOnly" x:Name="coreListPicker"

            FullModeItemTemplate="{StaticResource PickerFullModeItemTemplate}"

            CacheMode="BitmapCache" ItemTemplate="{StaticResource ListPickerItemTemplate}">

        </toolkit:ListPicker>

    </Grid>

    
    <StackPanel Orientation="Horizontal">

        <Button x:Name="btnPromptOK" Click="btnPromptOK_Click_1" Width="240" Content="ok"></Button>

        <Button x:Name="btnPromptCancel" Click="btnPromptCancel_Click_1" Width="240" Content="cancel"></Button>

    </StackPanel>

</StackPanel>

Notice the default top margin offset, that puts the StackPanel above the visible area. This will be used later to create the slide-in effect. The TextBlock element that carries the message header is bound to the PromptText dependency property.

DependencyProperty _promptTextProperty = DependencyProperty.Register("PromptText", typeof(string),

    typeof(ToastPrompt), new PropertyMetadata(string.Empty));

public string PromptText

{

    get

    {

        return (string)GetValue(_promptTextProperty);

    }

    set

    {

        SetValue(_promptTextProperty, value);

    }

}

Looking at the Grid control that follows, you can see that I am overlaying a TextBox and a ListPicker control. Their visibility is bound to the PromptType property, that determines which of the two will be the active input receiver.

DependencyProperty _promptTypeProperty = DependencyProperty.Register("PromptType", typeof(PromptType),

    typeof(ToastPrompt), new PropertyMetadata(PromptType.Text));

public PromptType PromptType

{

    get

    {

        return (PromptType)GetValue(_promptTypeProperty);

    }

    set

    {

        SetValue(_promptTypeProperty, value);

    }

}

PromptType itself is an enum, that can be set to be either Text or Selector:

public enum PromptType

{

    Text,

    Selector

}

Now that there is the basic visual tree, I need to also bind the TextBox to an internal string property and the ListPicker to a collection that will present the user with the available options. Falling back to dependency properties once again, I have PromptResult:

DependencyProperty _promptResult = DependencyProperty.Register("PromptResult", typeof(string),

    typeof(ToastPrompt), new PropertyMetadata(string.Empty));

public string PromptResult

{

    get

    {

        return (string)GetValue(_promptResult);

    }

    set

    {

        SetValue(_promptResult, value);

    }

}

And ItemsSource:

DependencyProperty _itemsSourceProperty = 

DependencyProperty.Register("ItemsSource", typeof(System.Collections.IEnumerable),

    typeof(ToastPrompt), new PropertyMetadata(null));

public System.Collections.IEnumerable ItemsSource

{

    get

    {

        return (System.Collections.IEnumerable)GetValue(_itemsSourceProperty);

    }

    set

    {

        SetValue(_itemsSourceProperty, value);

    }

}

In many cases, as the prompt is introduced in a page, I need to track whether it is open or not. For that purpose, there is the IsOpen flag:

DependencyProperty _isOpenProperty = DependencyProperty.Register("IsOpen", typeof(bool),

    typeof(ToastPrompt), new PropertyMetadata(false));

public bool IsOpen

{

    get

    {

        return (bool)GetValue(_isOpenProperty);

    }

    set

    {

        SetValue(_isOpenProperty, value);

    }

}

Let’s add the sliding in/out animations. Since I am already heavily relying on XAML, I can simply use Storyboard instances with DoubleAnimationUsingKeyFrames (placed in the UserControl resources):

<Storyboard x:Key="CustomSetPromptAnimation">

    <DoubleAnimationUsingKeyFrames 

      Duration="0:0:0.25"

      Storyboard.TargetName="stkCustomSetTranslateTransform"

      Storyboard.TargetProperty="Y"

      >

        <SplineDoubleKeyFrame Value="0"

          KeyTime="0:0:0" KeySpline="0.25,0.5,0.75,1" />

        <SplineDoubleKeyFrame Value="260"

          KeyTime="0:0:0.25" KeySpline="0.25,0.5,0.75,1" />

    </DoubleAnimationUsingKeyFrames>

</Storyboard>


<Storyboard x:Key="CustomSetPromptAnimationReverse">

    <DoubleAnimationUsingKeyFrames 

      Duration="0:0:0.25"

      Storyboard.TargetName="stkCustomSetTranslateTransform"

      Storyboard.TargetProperty="Y"

      >

        <SplineDoubleKeyFrame Value="260"

          KeyTime="0:0:0" KeySpline="0.25,0.5,0.75,1" />

        <SplineDoubleKeyFrame Value="0"

          KeyTime="0:0:0.25" KeySpline="0.25,0.5,0.75,1" />

    </DoubleAnimationUsingKeyFrames>

</Storyboard>

Looking back at the core control XAML, you already saw that the visibility of the input controls is determined through a converter – TypeToVisibilityConverter:

public class TypeToVisibilityConverter : IValueConverter

{

    public object Convert(object value, Type targetType, object parameter,

System.Globalization.CultureInfo culture)

    {

        PromptType type = (PromptType)value;

        if (type == PromptType.Selector)

        {

            if (parameter != null && parameter.ToString() == "text")

                return Visibility.Collapsed;

            else

                return Visibility.Visible;

        }

        else

        {

            if (parameter != null && parameter.ToString() == "text")

                return Visibility.Visible;

            else

                return Visibility.Collapsed;

        }

    }


    public object ConvertBack(object value, Type targetType, 

object parameter, System.Globalization.CultureInfo culture)

    {

        throw new NotImplementedException();

    }

}

In order to avoid using two converter classes, I am leveraging the parameter argument, that will tell the program what control check to run.

Opening and closing the prompt is a matter of animating the proper Storyboard instances, whether the original one, or the reverse one:

public void Open()

{

    Storyboard promptAnimation = (Storyboard)this.Resources["CustomSetPromptAnimation"];

    promptAnimation.Begin();

    IsOpen = true;

}


public void Close()

{

    Storyboard promptAnimation = (Storyboard)this.Resources["CustomSetPromptAnimationReverse"];

    promptAnimation.Begin();

    IsOpen = false;

}

Since the dialog can be closed by either cancelling the operation or accepting the input, I need to add Click handlers to both the OK and Cancel buttons.

private void btnPromptCancel_Click_1(object sender, RoutedEventArgs e)

{

    Close();

    if (PromptType == Controls.PromptType.Text)

    {

        txtPromptText.Text = string.Empty;

    }

}


private void btnPromptOK_Click_1(object sender, RoutedEventArgs e)

{

    if (PromptType == Controls.PromptType.Text)

    {

        OnInputClose(this, new EventArgs(txtPromptText.Text));

        txtPromptText.Text = string.Empty;

    }

    else

    {

        OnInputClose(this, new EventArgs(coreListPicker.SelectedItem.ToString()));

    }


    Close();

}

When the Cancel button is pressed, the dialog is dismissed. In case it was of type Text, the context of the input text box are cleared. If it is a Selector, for convenience purposes I am preserving the last selection. On OK, the OnInputClose event handler is called.

public delegate void InputCloseEventHandler(object sender, EventArgs e);

public event InputCloseEventHandler OnInputClose = delegate { };

It can be defined in the user code and not on the control side, and will be always triggered when the input was accepted. If you look closely at the event handler invocation code, you will see that I am taking a less standard approach when setting the EventArgs for it. Specifically, I am using generic EventArgs<T>, where T is a string, representing the current input. It is implemented through a custom class:

public class EventArgs : EventArgs

{

    public EventArgs(T value)

    {

        m_value = value;

    }


    private T m_value;


    public T Value

    {

        get { return m_value; }

    }

}

Now, the control can be simply added to any XAML page like this:

<controls:ToastPrompt x:Name="customToast" PromptText="add custom category"

PromptType="Text" OnInputClose="customToast_OnInputClose_1"></controls:ToastPrompt>

Download the XAML.

Download the C# Source.