2
votes

I want to create a "FlipPanel", which provides two different views of the same object. Here is the approach I am taking.

This is the main page, which consists of an ItemsControl whose ItemTemplate is a FlipPanel. The FlipPanel exposes two properties which define the DataTemplate to use for the Front and the Back.

<UserControl.Resources>

    <ControlTemplate x:Key="MyFlipTemplate">
        <StackPanel>
            <Button Content="Flip" x:Name="PART_FlipButton"/>
            <ContentPresenter Content="{TemplateBinding Content}" x:Name="PART_FlipContent"/>
        </StackPanel>
    </ControlTemplate>

    <DataTemplate x:Key="Front">
        <StackPanel Orientation="Horizontal">
            <TextBlock Text="Front"/>
            <TextBlock Text="{Binding Name}"/>
        </StackPanel>
    </DataTemplate>

    <DataTemplate x:Key="Back">
        <StackPanel>
            <TextBlock Text="Back"/>
            <TextBlock Text="{Binding Description}"/>
        </StackPanel>
    </DataTemplate>
</UserControl.Resources>

<StackPanel>
    <ItemsControl x:Name="_items">
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel></StackPanel>
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <SLTest:FlipPanel Template="{StaticResource MyFlipTemplate}" FrontDataTemplate="{StaticResource Front}" BackDataTemplate="{StaticResource Back}" Side="Front"/>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</StackPanel>

The Code Behind for the main page, is very simple as it just sets the DataContext of the ItemsControl to a list of Test Data.

using System.Collections.Generic;
using System.Windows.Controls;

namespace SLTest
{
    public partial class NewPage : UserControl
    {
        public NewPage()
        {
            InitializeComponent();

            _items.ItemsSource = Items;
        }

        public IList Items 
        {
            get
            {
                return new List
                           {
                               new NewClass { Name = "Name 1", Description = "Description 1"},
                               new NewClass { Name = "Name 2", Description = "Description 2"},
                               new NewClass { Name = "Name 3", Description = "Description 3"},
                               new NewClass { Name = "Name 4", Description = "Description 4"}
                           };
            }
        }
    }

    public class NewClass
    {
        public string Name;
        public string Description;
    }
}

The FlipPanel code is relatively simple as well, as it attempts to change the DataTemplate based on the Side DependencyProperty. The issue appears to be that the ContentPresenter's DataContext is lost at some point. In the code I have two comments that indicate the validity of the DataContext for the ContentPresenter.

using System;
using System.Windows;
using System.Windows.Controls;

namespace SLTest
{
    [TemplatePart(Name = FlipPanel.ButtonPart, Type = typeof(Button))]
    [TemplatePart(Name = FlipPanel.ContentPart, Type = typeof(ContentPresenter))]
    public partial class FlipPanel : ContentControl
    {
        private const string ButtonPart = "PART_FlipButton";
        private const string ContentPart = "PART_FlipContent";

        public enum FlipSide
        {
            Front,
            Back
        }

        private FlipSide _flipSide;

        public static readonly DependencyProperty SideProperty = DependencyProperty.RegisterAttached("FlipSide", typeof(FlipSide), typeof(FlipPanel), new PropertyMetadata(FlipSide.Front, FlipSidePropertyChanged));
        public static readonly DependencyProperty FrontDataTemplateProperty = DependencyProperty.Register("FrontDataTemplate", typeof (DataTemplate), typeof (FlipPanel), null);
        public static readonly DependencyProperty BackDataTemplateProperty = DependencyProperty.Register("BackDataTemplate", typeof(DataTemplate), typeof(FlipPanel), null);

        private Button _flipButton;
        private ContentPresenter _content;

        public FlipPanel()
        {
            InitializeComponent();
        }

        public DataTemplate FrontDataTemplate
        {
            get
            {
                return (DataTemplate) GetValue(FrontDataTemplateProperty);
            }
            set
            {
                SetValue(FrontDataTemplateProperty, value);
            }
        }

        public DataTemplate BackDataTemplate
        {
            get
            {
                return (DataTemplate)GetValue(BackDataTemplateProperty);
            }
            set
            {
                SetValue(BackDataTemplateProperty, value);
            }
        }

        private static void FlipSidePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var flipSide = (FlipSide)e.NewValue;
            var flipPanel = d as FlipPanel;

            flipPanel._content.ContentTemplate = flipSide == FlipSide.Front ? flipPanel.FrontDataTemplate : flipPanel.BackDataTemplate;
        }

        public override void OnApplyTemplate()
        {
            _flipButton = GetTemplateChild(ButtonPart) as Button;
            _flipButton.Click += OnFlipClicked;

            _content = GetTemplateChild(ContentPart) as ContentPresenter;

            // _content.DataContext is valid right here...

            _content.ContentTemplate = Side == FlipSide.Front ? FrontDataTemplate : BackDataTemplate;

            base.OnApplyTemplate();
        }

        private void OnFlipClicked(object sender, RoutedEventArgs e)
        {
            // _content.DataContext is now NULL!!!!

            Side = (Side == FlipSide.Front) ? FlipSide.Back : FlipSide.Front;
        }

        public FlipSide Side
        {
            get
            {
                return (FlipSide) GetValue(SideProperty);
            }
            set
            {
                SetValue(SideProperty, value);
            }
        }
    }
}

Any ideas?

I am not sure if this is the right approach to solving my requirement, if there is a better way I would welcome any further suggestions.

Thanks

1

1 Answers

2
votes

Chris,

I don't know if you're still looking for a way to do this. I know that there are a lot of guides to similar problems, but not exactly what you're looking for. From my understanding of what you want, you want a control which has two faces (really x faces) where a user can push a button and cause the panel to "flip" and show different data. However, you want this data that is shown to be generic enough so that this flip panel can be used in other locations with just a different implementation rather than totally different code. Do I understand your needs correctly? If not, please clarify where I've gone astray and I can maybe get a better answer for you. With that being said, here is what I've done (Google code demo project at bottom):

  1. I created a control library that houses my FlipPanel (because that is how I do things; so that I may use the controls in other projects down the road.)
  2. I styled the control in the control library to contain the above described properties that you need in your scenario.
  3. I created a Silverlight 2.0 application to create an instance of the control.
  4. I created a basic object for binding that has a few properties so that I can demonstrate the control's potential.

Here is a possible definition to use in a Silverlight 2.0 page:

    <Grid x:Name="LayoutRoot" Background="White">
  <controls:FlipPanel x:Name="TestingFlipPanel" Side="Front" >
   <controls:FlipPanel.BackDataTemplate>
    <DataTemplate>
     <StackPanel Orientation="Horizontal">
      <TextBlock Text="Back: "/>
      <TextBlock Text="{Binding BackText}" />
     </StackPanel>
    </DataTemplate>
   </controls:FlipPanel.BackDataTemplate>
   <controls:FlipPanel.FrontDataTemplate>
    <DataTemplate>
     <StackPanel Orientation="Horizontal">
      <TextBlock Text="Front: "/>
      <TextBlock Text="{Binding FrontText}" />
     </StackPanel>
    </DataTemplate>
   </controls:FlipPanel.FrontDataTemplate>
  </controls:FlipPanel>
    </Grid>

The alternative to this is to define the data templates in the user control (page level, or even the app level) like this:

So that you have an idea of what my binding object looks like here is that definition (yes, it's VB... Sorry!):

Public Class BindingObject
 Private _FrontText As String
 Private _BackText As String

 Public Sub New(ByVal frontText As String, ByVal backText As String)
  MyBase.New()
  _FrontText = frontText
  _BackText = backText
 End Sub

 Public Property FrontText() As String
  Get
   Return _FrontText
  End Get
  Set(ByVal value As String)
   _FrontText = value
  End Set
 End Property
 Public Property BackText() As String
  Get
   Return _BackText
  End Get
  Set(ByVal value As String)
   _BackText = value
  End Set
 End Property

End Class

In my code behind, here is the definition of my page and setting of the data context for the flip panel:

Partial Public Class Page
 Inherits UserControl
 Dim _BindingObject As New BindingObject("This is the front", "This is the back")
 Public Sub New()
  InitializeComponent()
 End Sub

 Private Sub Page_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
  TestingFlipPanel.DataContext = _BindingObject
 End Sub
End Class

So with this all laid out in front of you, we would expect that the control would display a button (in the control style) and a text block (actually two) that says "Front: This is the front" and when the button is pressed it is "flipped" to display "Back: This is the back".

With all that being said, here is the style that I used for the control:

    <Style TargetType="controls:FlipPanel">
  <Setter Property="Template">
   <Setter.Value>
    <ControlTemplate TargetType="controls:FlipPanel">
     <Grid x:Name="LayoutRoot">
      <Grid>
       <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
       </Grid.RowDefinitions>
       <Button x:Name="FlipButton" Content="Flip" Grid.Row="0" />
       <ContentPresenter Grid.Row="1" x:Name="FrontContentPresenter" Content="{TemplateBinding DataContext}" ContentTemplate="{TemplateBinding FrontDataTemplate}" />
       <ContentPresenter Grid.Row="1" x:Name="BackContentPresenter"  Content="{TemplateBinding DataContext}" ContentTemplate="{TemplateBinding BackDataTemplate}" />
      </Grid>

     </Grid>
    </ControlTemplate>
   </Setter.Value>
  </Setter>
 </Style>

And finally, the control's definition (Caution - It's long):

<TemplatePart(Name:=FlipPanel.LayoutRoot_ElementName, Type:=GetType(FrameworkElement))> _
<TemplatePart(Name:=FlipPanel.FrontContentPresenter_ElementName, Type:=GetType(FrameworkElement))> _
<TemplatePart(Name:=FlipPanel.BackContentPresenter_ElementName, Type:=GetType(FrameworkElement))> _
<TemplatePart(Name:=FlipPanel.FlipButton_ElementName, Type:=GetType(FrameworkElement))> _
Public Class FlipPanel
 Inherits Control

 Public Const LayoutRoot_ElementName As String = "LayoutRoot"
 Public Const FlipButton_ElementName As String = "FlipButton"
 Public Const FrontContentPresenter_ElementName As String = "FrontContentPresenter"
 Public Const BackContentPresenter_ElementName As String = "BackContentPresenter"

 Public Enum Sides
  Front
  Back
 End Enum

 Private _LayoutRoot As FrameworkElement = Nothing
 Private _FlipButton As FrameworkElement = Nothing
 Private _FrontContentPresenter As FrameworkElement = Nothing
 Private _BackContentPresenter As FrameworkElement = Nothing

 Private _ControlUpdating As Boolean = False

 Public Sub New()
  MyBase.New()
  MyBase.DefaultStyleKey = GetType(FlipPanel)
 End Sub

 Public Overrides Sub OnApplyTemplate()
  MyBase.OnApplyTemplate()
  UpdateControl()
 End Sub

 Private Sub UpdateControl()
  If _ControlUpdating Then Exit Sub
  _ControlUpdating = True

  If _LayoutRoot Is Nothing Then _LayoutRoot = TryCast(GetTemplateChild(LayoutRoot_ElementName), FrameworkElement)
  If _LayoutRoot IsNot Nothing Then
   Dim element As Grid = TryCast(_LayoutRoot, Grid)
   If element IsNot Nothing Then
    ' Update LayoutGrid here.  
   End If
  End If
  If _FlipButton Is Nothing Then _FlipButton = TryCast(GetTemplateChild(FlipButton_ElementName), FrameworkElement)
  If _FlipButton IsNot Nothing Then
   Dim element As Button = TryCast(_FlipButton, Button)
   If element IsNot Nothing Then
    ' Update Button
    RemoveHandler element.Click, AddressOf _FlipButton_Click
    AddHandler element.Click, AddressOf _FlipButton_Click
   End If
  End If
  If _FrontContentPresenter Is Nothing Then _FrontContentPresenter = TryCast(GetTemplateChild(FrontContentPresenter_ElementName), FrameworkElement)
  If _FrontContentPresenter IsNot Nothing Then
   Dim element As ContentPresenter = TryCast(_FrontContentPresenter, ContentPresenter)
   If element IsNot Nothing Then
    ' Update FrontContentPresenter here.
    If Side = Sides.Front Then
     element.Visibility = Windows.Visibility.Visible
    Else
     element.Visibility = Windows.Visibility.Collapsed
    End If
   End If
  End If
  If _BackContentPresenter Is Nothing Then _BackContentPresenter = TryCast(GetTemplateChild(BackContentPresenter_ElementName), FrameworkElement)
  If _BackContentPresenter IsNot Nothing Then
   Dim element As ContentPresenter = TryCast(_BackContentPresenter, ContentPresenter)
   If element IsNot Nothing Then
    ' Update BackContentPresenter here.  
    If Side = Sides.Front Then
     element.Visibility = Windows.Visibility.Collapsed
    Else
     element.Visibility = Windows.Visibility.Visible
    End If
   End If
  End If


  _ControlUpdating = False
 End Sub

 Private Sub _FlipButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
  Select Case Side
   Case Sides.Front
    Side = Sides.Back
   Case Sides.Back
    Side = Sides.Front
   Case Else
    Throw New ArgumentOutOfRangeException("Side")
  End Select
  UpdateControl()
 End Sub

 Private Sub FlipPanel_LayoutUpdated(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.LayoutUpdated
  UpdateControl()
 End Sub

#Region " FrontDataTemplateProperty Dependency Property "

#Region " FrontDataTemplate Property "

 Public Property FrontDataTemplate() As DataTemplate
  Get
   Return DirectCast(GetValue(FrontDataTemplateProperty), DataTemplate)
  End Get
  Set(ByVal value As DataTemplate)
   SetValue(FrontDataTemplateProperty, value)
  End Set
 End Property

#End Region

#Region " FrontDataTemplate Dependency Property "

 Public Shared ReadOnly FrontDataTemplateProperty As DependencyProperty = DependencyProperty.Register("FrontDataTemplate", GetType(DataTemplate), GetType(FlipPanel), New PropertyMetadata(Nothing, AddressOf OnFrontDataTemplatePropertyChanged))

#End Region

#Region " FrontDataTemplate Property Changed CallBack "

 Private Shared Sub OnFrontDataTemplatePropertyChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
  If e.OldValue Is Nothing AndAlso e.NewValue Is Nothing Then Exit Sub
  If e.OldValue IsNot Nothing AndAlso e.OldValue.Equals(e.NewValue) Then Exit Sub

  Dim source As FlipPanel = TryCast(d, FlipPanel)
  If source Is Nothing Then Throw New ArgumentException("source is not an instance of FlipPanel!")

  ' Provide any other validation here.

  ' Apply any other changes here.

 End Sub

#End Region

#End Region

#Region " BackDataTemplateProperty Dependency Property "

#Region " BackDataTemplate Property "

 Public Property BackDataTemplate() As DataTemplate
  Get
   Return DirectCast(GetValue(BackDataTemplateProperty), DataTemplate)
  End Get
  Set(ByVal value As DataTemplate)
   SetValue(BackDataTemplateProperty, value)
  End Set
 End Property

#End Region

#Region " BackDataTemplate Dependency Property "

 Public Shared ReadOnly BackDataTemplateProperty As DependencyProperty = DependencyProperty.Register("BackDataTemplate", GetType(DataTemplate), GetType(FlipPanel), New PropertyMetadata(Nothing, AddressOf OnBackDataTemplatePropertyChanged))

#End Region

#Region " BackDataTemplate Property Changed CallBack "

 Private Shared Sub OnBackDataTemplatePropertyChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
  If e.OldValue Is Nothing AndAlso e.NewValue Is Nothing Then Exit Sub
  If e.OldValue IsNot Nothing AndAlso e.OldValue.Equals(e.NewValue) Then Exit Sub

  Dim source As FlipPanel = TryCast(d, FlipPanel)
  If source Is Nothing Then Throw New ArgumentException("source is not an instance of FlipPanel!")

  ' Provide any other validation here.

  ' Apply any other changes here.

 End Sub

#End Region

#End Region

#Region " SideProperty Dependency Property "

#Region " Side Property "

 Public Property Side() As Sides
  Get
   Return DirectCast(GetValue(SideProperty), Sides)
  End Get
  Set(ByVal value As Sides)
   SetValue(SideProperty, value)
  End Set
 End Property

#End Region

#Region " Side Dependency Property "

 Public Shared ReadOnly SideProperty As DependencyProperty = DependencyProperty.Register("Side", GetType(Sides), GetType(FlipPanel), New PropertyMetadata(Sides.Front, AddressOf OnSidePropertyChanged))

#End Region

#Region " Side Property Changed CallBack "

 Private Shared Sub OnSidePropertyChanged(ByVal d As DependencyObject, ByVal e As DependencyPropertyChangedEventArgs)
  If e.OldValue Is Nothing AndAlso e.NewValue Is Nothing Then Exit Sub
  If e.OldValue IsNot Nothing AndAlso e.OldValue.Equals(e.NewValue) Then Exit Sub

  Dim source As FlipPanel = TryCast(d, FlipPanel)
  If source Is Nothing Then Throw New ArgumentException("source is not an instance of FlipPanel!")

  ' Provide any other validation here.

  ' Apply any other changes here.

 End Sub

#End Region

#End Region

End Class

Now, what you've been waiting for, the code!

Enjoy!

Google Code - http://code.google.com/p/stackoverflow-answers-by-scott/

Google Code - Source Code (Zip)

Thank you!