For a recent Windows Store App, I needed to create a way for people to log into it. I could have easily created a simple control or page that allowed the user to enter their login information, but I wanted to work with the new async/await architecture. I thought that creating a login dialog that fit this pattern would be a fun adventure. I wanted to follow the same pattern found in the MessageDialog class so it would be easy to use and understand how it works. This is part one of a two part blog. Part one will cover creating a LoginDialog class that only handles the ability to login. Part two will cover changing that class to be a more generic CustomDialog class that allows for custom content. By the end of the blog we’ll create a LoginDialog that will display a login to the user that looks a lot like the login you get from the People app.
This first stab at the dialog will focus on the ability to login, so we need a class that will hold the information the user enters.
public class LoginResult
{
public string Name { get; set; }
public string Password { get; set; }
}
Next, let’s stub out what the API will look like. As I said, I wanted this to follow the same pattern as the MessageDialog, so I’ll create a class that closely mirrors that functionality
public class LoginDialog
{
public LoginDialog(string title) { }
public LoginDialog(string title, string content) : this(title) { }
public IAsyncOperation<LoginResult> ShowAsync() { }
}
Notice that this is just a plain class. It is not a UIControl. This means that you cannot place this into your XAML, you can only create and show it from code. The LoginDialog is not a control, but it does need to display a control. There are two ways you could go about doing this. One is to create a class that is the control to display, the other is to create the control in code within the LoginDialog itself. The first is probably the easiest, but I wanted to accomplish this with only one class. I could have made an internal control that is only used by the LoginDialog (and this is the easier way to go and I’ll show using it later) but I wanted to try doing this with only one class. For now I’ll just stub out the creation of the actual control within the ShowAsync method. This method needs to return an IAsyncOperation of type LoginResult. To create an instance of IAsyncOperation you use the static Run method of the AsyncInfo.
private Popup _popup;
private TaskCompletionSource<LoginResult> _taskCompletionSource;
public IAsyncOperation<LoginResult> ShowAsync()
{
_popup = new Popup { Child = CreateLogin() };
if (_popup.Child != null)
{
_popup.IsOpen = true;
}
return AsyncInfo.Run(token => WaitForInput(token));
}
private Task<LoginResult> WaitForInput(CancellationToken token)
{
_taskCompletionSource = new TaskCompletionSource<LoginResult>();
token.Register(OnCanceled);
return _taskCompletionSource.Task;
}
The WaitForInput method allows me to create the TaskCompletionSource that will be used to return the result. TaskCompletionSource is an awesome class that allows you to set the result at a later time. The WaitForInput method returns the Task property of the TaskCompletionSource. This allows for some async goodness. The code will wait until the SetResult method is called on the TaskCompletionSource.
To set the result of TaskCompletionSource and ultimately of the ShowAsync method we need two methods, one for when the dialog is canceled and one for when the user has entered their information. The cancel method will handle if the user clicked the cancel button or, if the application cancels the IAsyncOperation within the code. Note: I tried using the cancelation token for the cancel method but that did not do anything.
private void OnCompleted()
{
var result = new LoginResult();
result.Name = _userTextBox.Text;
result.Password = _passwordTextBox.Password;
_popup.IsOpen = false;
_taskCompletionSource.SetResult(result);
}
private void OnCanceled()
{
// null to indicate dialog was canceled
LoginResult result = null;
_popup.IsOpen = false;
_taskCompletionSource.SetResult(result);
}
This is the basics of what we need, however windows store apps have so many visual states and sizes to be aware of. This means that the dialog needs to be aware of a few different events. If the app is on a small tablet the dialog needs to adjust for the virtual keyboard. If your app supports portrait the dialog needs to resize itself correctly.
// adjust for different view states
private void OnWindowSizeChanged(object sender, WindowSizeChangedEventArgs e)
{
if (_popup.IsOpen == false) return;
var child = _popup.Child as FrameworkElement;
if (child == null) return;
child.Width = e.Size.Width;
child.Height = e.Size.Height;
}
// Adjust the name/password textboxes for the virtual keyuboard
private void OnInputShowing(InputPane sender, InputPaneVisibilityEventArgs args)
{
var child = _popup.Child as FrameworkElement;
if (child == null) return;
var transform = _passwordTextBox.TransformToVisual(child);
var topLeft = transform.TransformPoint(new Point(0, 0));
// Need to be able to view the entire textblock (plus a little more)
var buffer = _passwordTextBox.ActualHeight + 10;
if ((topLeft.Y + buffer) > sender.OccludedRect.Top)
{
var margin = topLeft.Y - sender.OccludedRect.Top;
margin += buffer;
child.Margin = new Thickness(0, -margin, 0, 0);
}
}
private void OnInputHiding(InputPane sender, InputPaneVisibilityEventArgs args)
{
var child = _popup.Child as FrameworkElement;
if (child == null) return;
child.Margin = new Thickness(0);
}
We only want to subscribe to these events when the user shows the dialog (within the ShowAsync method), and we want to unsubscribe from the events when the dialog closes (OnCanceled and OnCompleted methods). Another event that is good to listen to is the KeyDown event of the window. If the user presses the escape key you should cancel the dialog.
private void SubscribeEvents()
{
Window.Current.SizeChanged += OnWindowSizeChanged;
Window.Current.Content.KeyDown += OnKeyDown;
var input = InputPane.GetForCurrentView();
input.Showing += OnInputShowing;
input.Hiding += OnInputHiding;
}
private void UnsubscribeEvents()
{
Window.Current.SizeChanged -= OnWindowSizeChanged;
Window.Current.Content.KeyDown -= OnKeyDown;
var input = InputPane.GetForCurrentView();
input.Showing -= OnInputShowing;
input.Hiding -= OnInputHiding;
}
The only thing I’ve left out is the creation of the actual dialog. For this I used a cool github project called XAML Conversion by Petr Onderka. I created the XAML in a user control and used this tool to convert it to code. For the most part it worked really well. It did require that you put children of panels into the Child tag.
<Border Background=""Red"" Height=""80"">
<Border.Child>
<TextBlock Text="Hello"/>
</Border.Child>
</Border>
Download the source and sample application today. This application takes advantage of the light styles I blogged about before.