In my last article on View-based navigation, I mentioned that a useful Navigation History module could be built to work in concert. This article discusses the design and implementation of that module. In this case my design goals are more straightforward:
- Allow the size of the navigation history to be set so as to control memory usage
- Allow going forwards and backwards in the history
- Allow inspection of the navigation history
- Allow aspects of navigation history to be bindable
Navigation History
As with the core navigation concepts I went over previously, there is some basic Navigation History built into the Frame class in Silverlight 3. Since I’ve extended the core navigation concepts I can also add additional hooks for navigation. As usual, I will first think of my design goals and craft an interface.
public interface INavigationHistory : INotifyPropertyChanged
{
/// <summary>
/// How many Views do we want to keep around?
/// </summary>
int HistorySize { get; set; }
/// <summary>
/// Go back one View, return the next active view
/// </summary>
IView GoBack();
/// <summary>
/// Go forward one view, return the next active view
/// </summary>
/// <returns></returns>
IView GoForward();
/// <summary>
/// Can we go back based on current position?
/// </summary>
bool CanGoBack{get;}
/// <summary>
/// Can we go forward based on current position
/// </summary>
bool CanGoForward{get;}
/// <summary>
/// Called by clients to add a view to history
/// </summary>
/// <param name="v"></param>
void Navigated(IView v);
event Action<IView> NavigatedToView;
event Action<IView> HistoryItemRemoved;
/// <summary>
/// Get the items in the current history, allow a jump to a specific point
/// </summary>
ObservableCollection<IView> CurrentHistory { get; }
/// <summary>
/// Go to a specific View in the history
/// </summary>
/// <param name="historyIndex"></param>
void GoTo(IView v);
/// <summary>
/// Removes the most recent item
/// </summary>
/// <returns></returns>
IView Pop();
}
Including INotifyPropertyChanged here means aspects of the navigation history will be bindable: CanGoBack, CanGoForward, and CurrentHistory. The other parts of the interface should be self explanatory.
Binding to History
Using the previous Audi Fans demo, I can now make the navigation buttons in the lower right hand corner work.
The XAML for creating these buttons is fairly simple, note the {Binding} aspects:
<StackPanel Orientation="Horizontal" Grid.Row="2" Margin="5" HorizontalAlignment="Right">
<TextBlock Style="{StaticResource TextStyle}" >Navigation: </TextBlock>
<Button x:Name="GoBack" IsEnabled="{Binding Path=History.CanGoBack}" Click="GoBack_Click">
<Button.Content>
<TextBlock Style="{StaticResource TextStyle}" Foreground="Black">< Back</TextBlock>
</Button.Content>
</Button>
<ComboBox x:Name="JumpCmb" DataContext="{Binding}" ItemsSource="{Binding Path=HistoryItems}" SelectedItem="{Binding Path=CurrentViewName, Mode=TwoWay}" Margin="10,0,10,0" Width="250" >
</ComboBox>
<Button x:Name="GoForward" IsEnabled="{Binding Path=History.CanGoForward}" Click="GoForward_Click">
<Button.Content>
<TextBlock Style="{StaticResource TextStyle}" Foreground="Black">Forward ></TextBlock>
</Button.Content>
</Button>
</StackPanel>
In order to satisfy these bindings, the Navigation History implementation has been added to the MainViewModel for the front page. You will note that I’m not binding directly to the History.CurrentHistory but that some additional fields have been added to MainViewModel:
ObservableCollection<string> _histItems;
/// <summary>
/// Silverlight crashes if you use a Page (IView) as a binding source. Lame.
/// </summary>
public ObservableCollection<string> HistoryItems
{
get
{
return _histItems;
}
}
string _currentViewName;
public string CurrentViewName
{
get { return _currentViewName; }
set
{
if (value != _currentViewName)
{
_currentViewName = value;
OnPropertyChanged("CurrentViewName");
if (_viewMap.ContainsKey(value))
{
Navigation.GoTo(_viewMap[value]);
}
}
}
}
Pit of failure: Silverlight does not seem to like having a Page object as a binding source except when doing Element binding. This may be a low level implementation detail that’s just not supported, or it may be because the Pages in question are not visible and this causes some fundamental failure.
So there is a layer that maps IView names back to the INavigationHistory because Silverlight binding doesn’t like binding to a visual element that is not currently visible.
Conclusion
That’s pretty much it for Navigation History, be sure to read the previous article if you’d like to see how it fits in with everything else. There are other directions you could take this, obviously. If you are using some kind of Region manager you could scope the navigation to each region. If you are concerned about memory usage, you could create a scheme to use WeakReferences and provide hooks to save just the ViewModel of an IView so that the IView itself could be reconstructed later.
The updated source code for the HandWaver.AG guidance projects can be found here.