Building slightly on the previous post, we can drill into properties and navigate back up the parent tree.The XAML adds some navigation hyperlinks and styling to differentiate betwwen a property that can drilled into vs one that can't (ValueTypes)
<UserControl x:Class="GoogleAuthDemo.ObjectBrowser"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:GoogleAuthDemo"
mc:Ignorable="d"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
d:DesignHeight="480" d:DesignWidth="480"
x:Name="root">
<UserControl.Resources>
<local:ObjectPropertiesConverter x:Key="ObjectPropertiesConvert"/>
<Style x:Key="PropertyStyle" TargetType="HyperlinkButton">
<Setter Property="Foreground" Value="{StaticResource PhoneForegroundBrush}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="FontSize" Value="{StaticResource PhoneFontSizeMedium}"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="HyperlinkButton">
<Border Background="Transparent">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
Storyboard.TargetName="TextElement">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource PhoneAccentBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="MouseOver"/>
<VisualState x:Name="Pressed">
<Storyboard>
<DoubleAnimation Duration="0" To="0.5"
Storyboard.TargetProperty="Opacity" Storyboard.TargetName="TextElement"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Foreground"
Storyboard.TargetName="TextElement">
<DiscreteObjectKeyFrame KeyTime="0"
Value="{StaticResource PhoneForegroundBrush}"/>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border Background="{TemplateBinding Background}"
Margin="{StaticResource PhoneHorizontalMargin}"
Padding="{TemplateBinding Padding}">
<TextBlock x:Name="TextElement"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
Text="{TemplateBinding Content}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Border>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<DataTemplate x:Key="PropertyTemplate">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
<TextBlock Text="{Binding Name}" Margin="0,0,10,1"/>
<HyperlinkButton HorizontalAlignment="Right"
IsEnabled="{Binding HasChildren}"
Content="{Binding Value}"
Click="HyperlinkButton_Click" Style="{StaticResource PropertyStyle}"/>
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<StackPanel Orientation="Vertical">
<HyperlinkButton Content="< Back" Foreground="{StaticResource PhoneAccentBrush}"
HorizontalAlignment="Left"
IsEnabled="{Binding ElementName=root, Path=CanBack}"
Click="BackButton_Click" FontWeight="Bold" FontStyle="Normal"/>
<ScrollViewer>
<ItemsControl ItemTemplate="{StaticResource PropertyTemplate}"
ItemsSource="{Binding Path=.,
Converter={StaticResource ObjectPropertiesConvert}}">
</ItemsControl>
</ScrollViewer>
</StackPanel>
</UserControl>
Then we need to add a little bit to the control's code behind to handle forward and backward navigation:
public partial class ObjectBrowser : UserControl
{
public ObjectBrowser()
{
InitializeComponent();
}
private Stack<object> _backStack = new Stack<object>();
private void HyperlinkButton_Click(object sender, RoutedEventArgs e)
{
if (this.DataContext != null)
{
_backStack.Push(this.DataContext);
CanBack = true;
}
ObjectProperty p = ((HyperlinkButton)sender).DataContext as ObjectProperty;
if (p != null)
this.DataContext = p.TheObject;
}
private void BackButton_Click(object sender, RoutedEventArgs e)
{
if (_backStack.Count > 0)
{
DataContext = _backStack.Pop();
CanBack = _backStack.Count > 0;
}
}
public const string CanBackPropertyName = "CanBack";
public bool CanBack
{
get
{
return (bool)GetValue(CanBackProperty);
}
set
{
SetValue(CanBackProperty, value);
}
}
public static readonly DependencyProperty CanBackProperty =
DependencyProperty.Register(
CanBackPropertyName,
typeof(bool),
typeof(ObjectBrowser),
new PropertyMetadata(false));
}
And we still need this converter class to take an object and break out its properties into something we can enumerate over to get the property name and instance's value:
public class ObjectPropertiesConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
if (value == null)
return null;
return from p in value.GetType().GetProperties()
where p.CanRead &&
p.GetIndexParameters().Count() == 0
select new ObjectProperty
{
Name = p.Name,
TheObject = p.GetValue(value, null)
};
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
But we're going to replace the KeyValuePair
with a small helper class. This is what each row in the browser will bind to and it keeps the actual object around to support navigation through the tree:
public class ObjectProperty
{
public object TheObject { get; set; }
public string Name { get; set; }
public string Value
{
get
{
return TheObject != null ? TheObject.ToString() : "(null)";
}
}
public bool HasChildren
{
get
{
if (TheObject != null)
return !TheObject.GetType().IsValueType;
return false;
}
}
}
So with that, you should be able to navigate object hierarchies from within the phone app at runtime. I wouldn't use it in an app, but I'm hoping it will be good debugging and rapid prototyping tool so I can build the data layer and rough UI structure and then deal with UI styling later.
** Warning - I'm posting as I code this so test coverage is well, um... limited **
CodeProject