This is part 2 of a series of blog posts on the design and implementation of the location-aware Windows Store app "Location Data Logger". Download the source code to Location Data Logger here.
The Main Page
Location Data Logger is a fairly simple application, so a single page is sufficient for its user interface. The operational controls such as the start/stop button and precision filter are placed in a sidebar for fast and easy access. The main content area holds either the map display or the grid display, and the user can toggle between them. This approach allows the content view to expand to fill the available screen space without having to know the display resolution. Finally, a lower app bar allows the user to set configuration items.
The screenshot, below, shows how the page is divided up. The top row is from Microsoft's default template which leaves the first 140 pixels clear of main content. It is below that where the layout gets more interesting. The lower grid consists of two columns: a 320 pixel sidebar, and the main content area.
The width of the sidebar was not chosen arbitrarily. When a Windows Store app is running in a snapped view, it is assigned 320 pixels on the screen, no matter which side it was placed on. Location Data Logger is designed to fit cleanly in the snapped view by placing its primary operational controls and status information in this sidebar. Thus, the user still has complete control over the app's operation, as well as a useful feedback on its progress.
The XAML for the grid layout is:
<Grid Style="{StaticResource LayoutRootStyle}">
<Grid.RowDefinitions>
<RowDefinition Height="140"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="1" Margin="0,0,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="320"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
</Grid>
The bottom app bar is where less frequently needed configuration items are kept, such as the selected log formats and the folder picker for setting the log file directory.
<common:LayoutAwarePage.BottomAppBar> <AppBar> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Button Grid.Column="1" x:Name="buttonSaveDir" HorizontalAlignment="Right" VerticalAlignment="Bottom" Style="{StaticResource FolderAppBarButtonStyle}" Click="buttonSaveDir_Click"/> <StackPanel x:Name="panelLogOptions" Grid.Column="0" Orientation="Horizontal"> <TextBlock Text="Log as:" VerticalAlignment="Center" Style="{StaticResource ItemTextStyle}" Margin="0,0,0,10"/> <ToggleButton x:Name="toggleCSV" VerticalAlignment="Center" Content="CSV" Margin="10,0,0,0" Click="toggleCSV_Click" IsChecked="True"/> <ToggleButton x:Name="toggleKML" VerticalAlignment="Center" Content="KML" Margin="10,0,0,0" Click="toggleKML_Click"/> <ToggleButton x:Name="toggleGPX" VerticalAlignment="Center" Content="GPX" Margin="10,0,0,0" Click="toggleGPX_Click"/> </StackPanel> </Grid> </AppBar> </common:LayoutAwarePage.BottomAppBar>
First Time Execution
When the application is launched for the very first time it is possible that the user will start the logger without first configuring the app, and that means that there must be reasonable default options. However, there is one important configuration option that cannot be set automatically, and that is the folder where the log files will be written.
The user's main document library is the obvious choice for a default, and it is tempting to try and hardcode it by using KnownFolders.DocumentsLibrary, but this requires that the Documents Library capability be declared in the manifest. The problem is that Microsoft has placed severe restrictions on its use, specifically:
The only acceptable use for the documentsLibrary capability is to support the opening of embedded content within another document.
The use of this capability is subject to Store policy, and a Windows Store app may not be approved if used improperly, so an alternate solution is necessary. What I choose to do is present a folder picker to the user if no log directory has been set at the time that they start the logger.
From MainPage.xaml.cs, in toggleStartStop_Click():
// Make sure we have a log directory/folder set if (logger.GetFolder() == null) { Boolean prompt_for_folder = true; // Check to see if we defined a folder in a previous session. try { String logdirToken = StorageApplicationPermissions.FutureAccessList.Entries.First().Token; StorageFolder folder = await StorageApplicationPermissions.FutureAccessList.GetFolderAsync(logdirToken); if (folder != null) { logger.SetFolder(folder); prompt_for_folder = false; } } catch { } // Prompt the user to choose a folder if one hasn't been set previously. if ( prompt_for_folder ) await set_logdir(); if (logger.GetFolder() == null) { // The user has not set a folder, of the previously set location no longer exists. toggleStartStop.IsChecked = false; // Uncheck the toggle button. return; } }
This code works by storing the most recent log directory as the only item on the FutureAccessList.
- If the user has never set a log directory, then the FutureAccessList is empty and we set prompt_for_folder to true.
- If they have set a log directory, then it simply takes the first (and only) directory token on the list and uses it to find the log directory. If the log directory does not exist, e.g. if it has been deleted, then set prompt_for_folder to true. Otherwise, set prompt_for_folder to false
Transitions and Animations
Microsoft encourages the use of animation to give a Windows Store app a fluid look, and to call out attention to significant items which change on screen. Location Data Logger incorporates animations in two key areas.
Transition between the map view and the data point view
The map and the data points are actually displayed in the same grid cell and the Collapsed property is used to determine which one is visible. When the map view is active, it's Collapsed property is set to Visible and the data point display is set to Collapsed, and visa-versa. A smooth crossfade between the two is done via storyboard animation on the Opacity property:
<Grid.Resources> <Storyboard x:Name="mapFadeIn"> <DoubleAnimation From="0" To="1" Duration="0:0:0.25" Storyboard.TargetName="gridMap" Storyboard.TargetProperty="Opacity"/> </Storyboard> <Storyboard x:Name="mapFadeOut" Completed="mapFadeOut_Completed"> <DoubleAnimation From="1" To="0" Duration="0:0:0.25" Storyboard.TargetName="gridMap" Storyboard.TargetProperty="Opacity"/> </Storyboard> <Storyboard x:Name="pointsFadeIn"> <DoubleAnimation From="0" To="1" Duration="0:0:0.25" Storyboard.TargetName="gridData" Storyboard.TargetProperty="Opacity"/> </Storyboard> <Storyboard x:Name="pointsFadeOut" Completed="pointsFadeOut_Completed"> <DoubleAnimation From="1" To="0" Duration="0:0:0.25" Storyboard.TargetName="gridData" Storyboard.TargetProperty="Opacity"/> </Storyboard> </Grid.Resources>
And some code to set the Collapsed property to Visible or Collapsed as appropriate.
private void buttonMap_Click(object sender, RoutedEventArgs e) { buttonMap.Style = (Style)Resources["ActiveItemTextButtonStyle"]; buttonPoints.Style = (Style)Resources["InactiveItemTextButtonStyle"]; if (gridMap.Opacity < 1) { mapFadeIn.Begin(); gridMap.Visibility = Windows.UI.Xaml.Visibility.Visible; } if ( gridData.Opacity > 0 ) pointsFadeOut.Begin(); } private void buttonPoints_Click(object sender, RoutedEventArgs e) { buttonMap.Style = (Style)Resources["InactiveItemTextButtonStyle"]; buttonPoints.Style = (Style)Resources["ActiveItemTextButtonStyle"]; if ( gridMap.Opacity > 0 ) mapFadeOut.Begin(); if (gridData.Opacity < 1) { pointsFadeIn.Begin(); gridData.Visibility = Windows.UI.Xaml.Visibility.Visible; } } private void mapFadeOut_Completed(object sender, object e) { gridMap.Visibility = Windows.UI.Xaml.Visibility.Collapsed; } private void pointsFadeOut_Completed(object sender, object e) { gridData.Visibility = Windows.UI.Xaml.Visibility.Collapsed; }
When the user switches from, say, the map view to the data point view, the data point grid is made Visible, and its Opacity is animated from 0.0 to 1.0. Simultaneously, the Opacity of the map is animated from 1.0 to 0.0. When the animation finishes, it is Collapsed.
Some of you may be wondering why I set the Collapsed property after that animation completes. The answer is because the two controls (the map and the data point grid) are overlaid on top of one another, and this can cause problems with mouse and touch events reaching the correct control. It's not enough to simply change the opacity: to make sure that UI events are applied to the visible control, the invisible control needs to be explicitly collapsed. A control with an Opacity of 0.0 is still an active control that can receive UI events.
Starting and stopping operation
When the logger is running, the status area displays a text box indicating the name of the log file and where it is being stored. It also serves as an indicator that the logger is active. Rather than just appear and disappear when the logger is started and stopped, however, storyboard animation is used to do a gradual fade in and fade out. This is very similar to the procedure above, only it is not necessary to collapse the UI element at the end. If the user hits "Reset" while the logger is running, a much faster fadeout occurs to draw attention to the fact that the log file has changed.
<StackPanel.Resources> <Storyboard x:Name="textLoggingInfoFadeIn"> <DoubleAnimation From="0" To="1" Duration="0:0:0.5" Storyboard.TargetName="textLoggingInfo" Storyboard.TargetProperty="Opacity"/> </Storyboard> <Storyboard x:Name="textLoggingInfoFadeOut"> <DoubleAnimation From="1" To="0" Duration="0:0:0.5" Storyboard.TargetName="textLoggingInfo" Storyboard.TargetProperty="Opacity"/> </Storyboard> <Storyboard x:Name="textLoggingInfoBlinkOut"> <DoubleAnimation From="1" To="0" Duration="0:0:0.125" Storyboard.TargetName="textLoggingInfo" Storyboard.TargetProperty="Opacity"/> </Storyboard> </StackPanel.Resources>
Next Up: The DataLogger Class
In Part 3, I'll dive into the code for the core of the application: the DataLogger object and the Geolocation sensor.
← Part1: Application Design | Part 3: The DataLogger Class → |