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 **

A simple SilverLight object viewer

Sometimes when you're making a windows phone 7 rest client you just want to look at some of the returned data to poke see what's there. You can wire up some ui or use tracing. Maybe some breakpoints. But it's also nice to be able to drop something into the UI that you can access easily and as needed. The WinForms object browser is a handy for instance for debugging things at runtime, within the app without a debugger or a designed UI. This little widget isn't as fully functional as taht but can be helpful in a similar way. Drop it somewhere in the UI and set its DataContext property and all public readable properies will be listed with name and value. First some XAML
<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">
    
    <UserControl.Resources>
        <local:ObjectPropertiesConverter x:Key="ObjectPropertiesConvert"/>
        
        <DataTemplate x:Key="PropertyTemplate">
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Stretch">
                <TextBlock Text="{Binding Key}" Margin="0,0,10,1"/>
                <TextBlock Text="{Binding Value}" TextWrapping="Wrap" TextAlignment="Right" HorizontalAlignment="Right" />
            </StackPanel>
        </DataTemplate>
    </UserControl.Resources>
    
    <ScrollViewer>
        <ItemsControl ItemTemplate="{StaticResource PropertyTemplate}"
                      ItemsSource="{Binding Path=., Converter={StaticResource ObjectPropertiesConvert}}">                
        </ItemsControl>
    </ScrollViewer>

</UserControl>

With a smidge of C# to create a name value pair collection, given an object's properties
    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
                   select new KeyValuePair<string, string>
                   (
                      p.Name,
                      p.GetValue(value, null) != null ? p.GetValue(value, null).ToString() : null
                   );                   
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }