Saturday, January 28, 2012

A slightly less simple object browser for windows phone 7

Building slightly on the previous post we can drill into properties and navigate back up the parent tree. The Xaml adds some styling and a couple of hyperlinks:
<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 little bit to the control 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;
            }
        }

        /// 
        /// The  dependency property's name.
        /// 
        public const string CanBackPropertyName = "CanBack";

        /// 
        /// Gets or sets the value of the 
        /// property. This is a dependency property.
        /// 
        public bool CanBack
        {
            get
            {
                return (bool)GetValue(CanBackProperty);
            }
            set
            {
                SetValue(CanBackProperty, value);
            }
        }

        /// 
        /// Identifies the  dependency property.
        /// 
        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 theproperty 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 // skip indexer properties
                   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;// && !(TheObject is string);

                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 rought UI structure and then deal with UI styling later.
** warning - I'm posting as I code this so test coverage is well, um... limited **

No comments:

Post a Comment