Tuesday, December 12, 2006

Consumer's application in WPF

You really want to learn WPF? Really want? Build consumer's application. Inspired of this BoingBoing post, I decided to build real-world application. Know what? This really small app will help you to understand WPF's power. So let's start.

In order to be able to convert text values into binary representation and be able to bind all this stuff to somewhere we'll have to write converter. So, let's do it. What is converter? Converter is class, that implements IValueConverter interface with two methods: Convert and ConvertBack. Stange, ah? :) In our case we got string and convert it into other string. So, follow the code to do it.

public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)

       {
           string origStr = value as string;
           string outStr = "";
           if (value != null)
           {
               foreach (char ch in origStr)
               { 
                   outStr += System.Convert.ToString((int)ch,2).ToString();
               }
           }
 
           return outStr;
       }

Now, when we can write somewhere and see our "binary" result somewhere else, we can tell our application, that the string source is textbox, and bind the results to some textblock. But first, we should annonce in our XAML, that we have converter and want to use it. The way is straight forward: our namespace declaration, static resource declaration, binding. Here the code:


<Window x:Class="BinBanner.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:m="clr-namespace:BinBanner" 
    Title="BinBanner" Height="600" Width="700" Style="{DynamicResource normal}" Name="myWindow"
    >
 
<Window.Resources>
     <m:Text2ValueConverter x:Key="conv"/>
 
<TextBox Name="SourceText" Width="400" MaxLength="30"/>
 
<TextBlock Name="tText" FontSize="12" Foreground="White" 
VerticalAlignment="Center" HorizontalAlignment="Left" Width="176" 
Height="240" TextWrapping="Wrap" FontFamily="Lucida Sans" 
TextAlignment="Center" Margin="40" 
Text="{Binding ElementName=SourceText, Path=Text, Converter={StaticResource conv}}"/>

That's it. Now we have simple program, to convert regular string into binary representation. But we want something "more spicy". Let's draw it on t-shirt!


In order to "draw" any WPF control on other control we have VisualBrush. What do you think, it's doing? Yeah, it takes Visual and convert it to Brush. So let put out TextBlock into VisualBrush and draw on T-Shirt image. How pathetic! We want it more "spicy". Let draw our text on 3D t-shirt! We'll put Viewport3D into our program and make 3D Geometry. In order to create 3d mesh of t-shirt we'll need 3d editor. I used Bruce3d to do it. You can use whatever you want. But how we'll convert the mesh to XAML? Oh, god, thank you, WPF Conversion Contest. Oky. Now we have our 3d t-shirt and dynamic text on it. You can put into VisualBrush anything. Even MediaElement. This one will play videos over your t-shirts ;)

<GeometryModel3D.Material>
                    <DiffuseMaterial>
                      <DiffuseMaterial.Brush>
                        <VisualBrush>
                          <VisualBrush.Visual>
                            <Grid Name="tGrid" Background="Black" Width="500" Height="400">
 
                              <TextBlock Name="tText" FontSize="12" Foreground="White" VerticalAlignment="Center" HorizontalAlignment="Left" Width="176" Height="240" TextWrapping="Wrap" FontFamily="Lucida Sans" TextAlignment="Center" Margin="40" Text="{Binding ElementName=SourceText, Path=Text, Converter={StaticResource conv}}"/>
                            </Grid>
                          </VisualBrush.Visual>
                        </VisualBrush>
                      </DiffuseMaterial.Brush>
                    </DiffuseMaterial>
</GeometryModel3D.Material>

 


Good, now it's time to make our customer to be able to choose a color of our t-shirt. For this propose, well create smal "smart" enum (as like as Colors or Brushes class do). It'll have static constructors to get everything we need about the color.


public sealed class ColorPickColors
    {
        private ColorPickColors(){}
 
        static bool m_reverse = false;
        static Color m_color;
        static string m_name;
 
        public bool IsColorDark
        {
            get { return !m_reverse; }
        }
 
        public Color GetColor
        {
            get { return m_color; }
        }
 
        public string GetName
        {
            get { return m_name; }
        }
 
        public static ColorPickColors Black { get { ColorPickColors cpc = new ColorPickColors(); m_name = "Black"; m_color = Colors.Black; m_reverse = false; return cpc; } }
        public static ColorPickColors White { get { ColorPickColors cpc = new ColorPickColors(); m_name = "White"; m_color = Colors.White; m_reverse = true; return cpc; } }
        public static ColorPickColors Red { get { ColorPickColors cpc = new ColorPickColors(); m_name = "Red"; m_color = Colors.Red; m_reverse = false; return cpc; } }
        public static ColorPickColors Pink { get { ColorPickColors cpc = new ColorPickColors(); m_name = "Pink"; m_color = Colors.Pink; m_reverse = true; return cpc; } }
        public static ColorPickColors CityGreen { get { ColorPickColors cpc = new ColorPickColors(); m_name = "City Green"; m_color = Color.FromRgb(0x5C, 0x52, 0x39); m_reverse = false; return cpc; } }
        public static ColorPickColors HotPink { get { ColorPickColors cpc = new ColorPickColors(); m_name = "Hot Pink"; m_color = Color.FromRgb(0xDB, 0x00, 0x68); m_reverse = false; return cpc; } }
        public static ColorPickColors Turquoise { get { ColorPickColors cpc = new ColorPickColors(); m_name = "Turquoise"; m_color = Color.FromRgb(0x09, 0xAB, 0x9C); m_reverse = false; return cpc; } }
        public static ColorPickColors BabyBlue { get { ColorPickColors cpc = new ColorPickColors(); m_name = "Baby Blue"; m_color = Color.FromRgb(0x92, 0xBA, 0xDD); m_reverse = true; return cpc; } }
        public static ColorPickColors DeepHeather { get { ColorPickColors cpc = new ColorPickColors(); m_name = "Deep Heather"; m_color = Color.FromRgb(0x83, 0x7B, 0x79); m_reverse = true; return cpc; } }
        public static ColorPickColors Chocolate { get { ColorPickColors cpc = new ColorPickColors(); m_name = "Chocolate"; m_color = Color.FromRgb(0x3F, 0x2A, 0x25); m_reverse = false; return cpc; } }
        public static ColorPickColors LimeWedge { get { ColorPickColors cpc = new ColorPickColors(); m_name = "Lime Wedge"; m_color = Color.FromRgb(0xBC, 0xF6, 0xBC); m_reverse = true; return cpc; } }
 

Then we'll create the Visual to display it's values.


class ColorPick : FrameworkElement
{
    Color m_color = Colors.Black;
    Color m_fcolor = Colors.White;
    Brush fb, bb;
    Pen p;
    FormattedText m_name;
    ColorPickColors m_picker = ColorPickColors.Black;
 
    public Brush ElementBrush
    {
        get { return bb; }
    }
 
    public Brush TextBrush
    {
        get { return fb; }
    }
 
    public Color ElementColor
    {
        get { return m_color; }
    }
    public Color TextColor
    {
        get { return m_fcolor; }
    }
 
    public ColorPickColors SetColor
    {
        set
        {
            m_picker = value;
            m_color = m_picker.GetColor;
            bb = new SolidColorBrush(m_color);
            bb.Freeze(); 
            if (!m_picker.IsColorDark)
                m_fcolor = Colors.Black;
 
            fb = new SolidColorBrush(m_fcolor);
            fb.Freeze();
            p = new Pen(fb, 2);
            p.Freeze(); 
            m_name = new FormattedText(m_picker.GetName, System.Globalization.CultureInfo.CurrentCulture,
                FlowDirection.LeftToRight,
                new Typeface(new FontFamily(), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal),
                12, fb);
        }
    }
 
    public ColorPickColors GetColor
    {
        get { return m_picker; }
    }
 
    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);
        Rect r = new Rect(0,0,this.Width, this.Height);
        drawingContext.DrawRoundedRectangle(bb, p, r, 10, 10);
 
        if (m_name == null)
            SetColor = ColorPickColors.Black;
        drawingContext.DrawText(m_name,
            new Point(this.Width/2 - m_name.Width / 2, this.Height/2 - m_name.Height / 2)
            );
    }
 
}

 


I do not want manually add each of those controls for each color, so let's add some logic to ColorPickerColors to make it able to tell us about what colors it has. As well we'll add method to parse string input and return appropirate color.

public static string[] GetValues()
 {
     return GetValues(ColorPickColors.Black);
 }
 public static string[] GetValues(ColorPickColors obj)
 {
     Type t = obj.GetType();
     MemberInfo[] mi = t.GetMember("*",MemberTypes.Property, BindingFlags.Public | BindingFlags.Static);
     List<string> s = new List<string>();
     for (int i = 0; i < mi.Length; i++)
     {
         if (mi[i].DeclaringType == typeof(ColorPickColors) & mi[i].Name != "Parse")
         {
             s.Add(mi[i].Name);
         }
     }
 
     return s.ToArray();
 }
 
 public static ColorPickColors Parse(string name)
 {
     ColorPickColors res = ColorPickColors.Black;
     if (TryParse(name, out res))
     {
         return res;
     }
     else
     {
         throw new ArgumentOutOfRangeException();
     }
 }
 public static bool TryParse(string name, out ColorPickColors result)
 {
     MemberInfo[] mi = typeof(ColorPickColors).GetMember("*", MemberTypes.Property, BindingFlags.Public | BindingFlags.Static);
     for (int i = 0; i < mi.Length; i++)
     {
         if (mi[i].Name == name)
         {
             PropertyInfo prop = typeof(ColorPickColors).GetProperty(mi[i].Name);
             result = prop.GetValue(ColorPickColors.Black, null) as ColorPickColors;
             if (result != null)
             {
                 return true;
             }
             else 
             {
                 return false;
             }
 
         }
     }
     result = null;
     return false;
 }
 
Very good. The only thinkg I should do now is to add all my color controls to the application. How, when our classes are smarter, it's link a peace of cake
 
string[] _colors = ColorPickColors.GetValues();
            for (int i = 0; i < _colors.Length; i++)
            {
                ColorPick cp = new ColorPick();
                cp.SetColor = ColorPickColors.Parse(_colors[i]);
                cp.Width = 80;
                cp.Height = 30;
                cp.Margin = new Thickness(3);
                cp.HorizontalAlignment = HorizontalAlignment.Center;
                cp.PreviewMouseLeftButtonDown += new MouseButtonEventHandler(cp_PreviewMouseLeftButtonDown);
                cp.PreviewMouseMove += new MouseEventHandler(cp_PreviewMouseMove);
                myColors.Children.Add(cp);
            }

I want to drag and drop those controls to my beautifull t-shirt to make my customer to be happy with UI. We'll implement it very easy way, without adorners and half opaqe controls, just standard darg and drop

void myViewport3D_DragOver(object sender, DragEventArgs e)
{
    if (!(e.Data.GetData("Color") is ColorPick))
    {
        DragDrop.DoDragDrop(this, e.Data, DragDropEffects.None);
    }
}
 
void myViewport3D_Drop(object sender, DragEventArgs e)
{
    mPick = ((ColorPick)e.Data.GetData("Color")).GetColor;
    tGrid.Background = ((ColorPick)e.Data.GetData("Color")).ElementBrush;
    tText.Foreground = ((ColorPick)e.Data.GetData("Color")).TextBrush;
    tDock.Background = ((ColorPick)e.Data.GetData("Color")).TextBrush;
 
}
 
void cp_PreviewMouseMove(object sender, MouseEventArgs e)
{
    if (e.LeftButton == MouseButtonState.Pressed)
    {
        DataObject _do = new DataObject();
 
        _do.SetData("Color", sender);
 
 
        DragDrop.DoDragDrop(this, _do, DragDropEffects.Copy | DragDropEffects.Move);
 
    }
}
 
 

The only think we should do now is to add this stuff to our application

<Viewbox>
      <DockPanel Name="myColors" ToolTipService.IsEnabled="True" ToolTipService.InitialShowDelay="0" ToolTipService.BetweenShowDelay="0" ToolTipService.HasDropShadow="True">
        <DockPanel.ToolTip>
          <TextBlock Text="Drag one of those color tags and drop it into t-shirt model to change it color"/>
        </DockPanel.ToolTip>
      </DockPanel>
    </Viewbox>

 


Fine, how the customer can choose the color, but what's about the size? We'll have to make him able to choose t-shirt size as well. Not all of our customers know what is "small,large,x-large" etc. Some of them want see bust size or the t-shirt length. So, in order to support it, let's create class for sizes.


class ApparelSize

    {
        string size;
        public string Size
        {
            get { return size; }
            set { size = value; }
        }
 
        int bust;
        public int Bust
        {
            get { return bust; }
            set { bust = value; }
        }
 
        int length;
        public int Length
        {
            get { return length; }
            set { length = value; }
        }
        public ApparelSize() { }
        internal ApparelSize(string size, int bust, int length)
        {
            this.Size = size;
            this.Bust = bust;
            this.Length = length;
 
        }
 
    }

Now, just add it into collection to create checkboxes


class ApparelsSizes : Collection<ApparelSize>
    {
       public ApparelsSizes()
        {
            this.Add(new ApparelSize("Small", 36, 26));
            this.Add(new ApparelSize("Medium", 40, 27));
            this.Add(new ApparelSize("Large", 44, 28));
            this.Add(new ApparelSize("X-Large", 48, 29));
            this.Add(new ApparelSize("2X-Large", 52, 30));
        }
    }
Fine.

After all this stuff we have two more resources to be added to our application. Sizes and Colors. As well as not all of our customers know what is Inch, so we'll have to support him to convert it into centimeters. Yap. another converter and static resource. Now, we'll add three comboboxes and two radiobuttons, each combo will show size in different way. Checkboxes will provide the customer way to convert inches to centimeters and vv.

<StackPanel DataContext="{StaticResource sizeSource}"  Orientation="Horizontal" ToolTip="Select the size of t-shirt you want to order. Notice, these items have a more relaxed fit.">
      <ComboBox Name="sSelection" Margin="10" ItemsSource="{Binding}" ItemTemplate="{StaticResource tSize}" IsSynchronizedWithCurrentItem="True" ToolTip="Size"/>
      <ComboBox Margin="10" ItemsSource="{Binding}" ItemTemplate="{StaticResource tBust}" IsSynchronizedWithCurrentItem="True" ToolTip="Bust"/>
      <ComboBox Margin="10" ItemsSource="{Binding}" ItemTemplate="{StaticResource tLength}" IsSynchronizedWithCurrentItem="True" ToolTip="Length"/>
      <StackPanel Margin="5" Orientation="Vertical">
        <RadioButton Name="inch" IsChecked="True">Inches</RadioButton>
        <RadioButton>Cm</RadioButton>
      </StackPanel>

Our datasource is static list of all sizes, but we have to write ItemTemplates for our combos to bind each of them to what we really want it to be binded.

<DataTemplate x:Key="tSize">
        <TextBlock Text="{Binding Path=Size}"/>
      </DataTemplate>
      <DataTemplate x:Key="tBust">
        <TextBlock Name="tbb"  Text="{Binding Path=Bust}"/>
        <DataTemplate.Triggers>
          <DataTrigger Binding="{Binding ElementName=inch, Path=IsChecked}" Value="False">
            <Setter Property="Text" Value="{Binding Path=Bust, Converter={StaticResource incm}}" TargetName="tbb" />
          </DataTrigger>
        </DataTemplate.Triggers>
      </DataTemplate>
      <DataTemplate x:Key="tLength">
        <TextBlock Name="tbl" Text="{Binding Path=Length}"/>
        <DataTemplate.Triggers>
          <DataTrigger Binding="{Binding ElementName=inch, Path=IsChecked}" Value="False">
            <Setter Property="Text" Value="{Binding Path=Length, Converter={StaticResource incm}}" TargetName="tbl" />
          </DataTrigger>
        </DataTemplate.Triggers>
</DataTemplate>

Done. Now, our customer can choose the color, size and even convert sizes from one unit to other. The only think we should add is, right! checkout! We really want to sell it, not to make the customer option to choose and print it. So, let's make ShoppingItem. It have to know what the customer want to write on the t-shirt, what color he want and what size he wearing. We maybe want to add the image of the t-shirt he build for himself, to refer it later, if he'll have problems with the order.


class ShoppingItem
    {
        ColorPickColors _color;
        public ColorPickColors Color
        {
            get { return _color; }
            set { _color = value; }
        }
        ApparelSize _size;
        public ApparelSize Size
        {
            get { return _size; }
            set { _size = value; }
        }
        string _text;
        public string Text
        {
            get { return _text; }
            set { _text = value; }
        }
 
        public System.Windows.Controls.Image _thumb;
        public System.Windows.Controls.Image Thumbnail
        {
            get { return _thumb; }
        }
 
        public ShoppingItem(ColorPickColors color,ApparelSize size, string text, System.Windows.Controls.Image thumb) 
        {
            _size = size;
            _color = color;
            _text = text;
            _thumb = thumb;
        }
    }

Wow, now we have another problem. Our 3d t-shirt is Visual, but I want it Image... Those are two different worlds, two different base classes. What do you think, let's try to render our FrameworkElement into Image? Let's do it.


Image Visual2Image(FrameworkElement elem)
        {
            RenderTargetBitmap rt = new RenderTargetBitmap((int)elem.ActualWidth, (int)elem.ActualHeight, 96, 96, PixelFormats.Pbgra32);
            rt.Render(elem);
            Image img = new Image();
            img.Source = BitmapFrame.Create(rt);
 
            return img;
        }

Now we'll add three buttons to add and remove items and check out the customer's order. We want to prevent the customer to remove or checkout items, when he have nothing in his shopping cart. So we have to know when the shopping cart is empty, and when it's full.

class ShopBox : ObservableCollection<ShoppingItem>
    {
        protected override void OnPropertyChanged(System.ComponentModel.PropertyChangedEventArgs e)
        {
            base.OnPropertyChanged(e);
 
            if (e.PropertyName == "Count")
            { 
                this.OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("IsEmpty"));
                this.OnPropertyChanged(new System.ComponentModel.PropertyChangedEventArgs("IsFull"));
            }
 
        }
 
        public bool IsEmpty
        {
            get { return this.Count <= 0 ? true : false; }
        }
        public bool IsFull
        {
            get { return !IsEmpty; }
        }
    }

Let's add buttons with logic, binded to sopping cart? We have to remember, that the only way customer can press checkout is when he see our 3d model on the screen, so we'll have not only know, when the shopping cart is full, but also when he see whatever we want him to see. For this we'll use MultiBinding and some converter that tell us when viewport3d is visible.

<Button Margin="5" ToolTip="Click here to add your item to shopping basket" Click="onAdd" IsEnabled="{Binding Source={StaticResource shopBox}, Path=IsEmpty}">
  <Image Source="Images/add-to-basket.png"/>
</Button>
<Button Name="btnRemove" Margin="5" ToolTip="Click here to remove all items from shopping basket" Click="onRemove" IsEnabled="{Binding Source={StaticResource shopBox}, Path=IsFull}">
  <Image Source="Images/delete-from-basket.png"/>
</Button>
<Button Name="btnCheckout" Margin="5" ToolTip="Click here to check out" Click="onCheckout">
  <Image Source="Images/basket.png"/>
  <Button.IsEnabled>
    <MultiBinding Converter="{StaticResource widthToBoolConv}">
      <Binding Source="{StaticResource shopBox}" Path="IsFull"/>
      <Binding ElementName="myViewport3D" Path="Width"/>
      </MultiBinding>
    </Button.IsEnabled>
  </Button>
 
class WidthToBoolConverter : IMultiValueConverter
   {
 
       #region IMultiValueConverter Members
 
       public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
       {
           return ((double)values[1] == 0 ? false : true) & (bool)values[0];
       }

Another thing I don't like is grayed out buttons. It's not clear when I can and when I can not press it, if I have images. Let's fix it with styles

<Style TargetType="Button">
     <Style.Triggers>
       <Trigger Property="IsEnabled" Value="False">
         <Setter Property="Opacity" Value="0.5"/>
       </Trigger>
       <Trigger Property="IsMouseOver" Value="True">
         <Setter Property="Background" Value="White"/>
       </Trigger>
     </Style.Triggers>
   </Style>

Wow! now the customer can add and remove items and even checkout them. But, stop. We'll need to get some information regarding the customers. For example where to send and where to get payment. Let's make customer's class with all those information.


class CustomerInfo:INotifyPropertyChanged
    {
        #region properties
        private string firstName;
        public string FirstName
        {
            get { return firstName; }
            set { firstName = value; OnPropertyChanged("FirstName"); }
        }
 
        private string lastName;
        public string LastName
        {
            get { return lastName; }
            set { lastName = value; OnPropertyChanged("LastName"); }
        }
 
        private string email;
        public string Email
        {
            get { return email; }
            set { email = value; OnPropertyChanged("Email"); }
        }
 
 
        private DateTime? birthDay;
        public DateTime? BirthDay
        {
            get { return birthDay; }
            set { birthDay = value; OnPropertyChanged("BirthDay"); }
        }
 
        private string streetAddress;
        public string StreetAddress
        {
            get { return streetAddress; }
            set { streetAddress = value; OnPropertyChanged("StreetAddress"); }
        }
 
        private string city;
        public string City
        {
            get { return city; }
            set { city = value; OnPropertyChanged("City"); }
        }
 
        private Countries country;
        public Countries Country
        {
            get { return country; }
            set { country = value; OnPropertyChanged("Country"); }
        }
 
        private int? zip;
        public int? Zip
        {
            get { return zip; }
            set { zip = value; OnPropertyChanged("Zip"); }
        }
 
        private string creditCardNumer;
        public string CreditCardNumber
        {
            get { return creditCardNumer; }
            set { creditCardNumer = value; OnPropertyChanged("CreditCardNumber"); OnPropertyChanged("CreditCardType"); }
        }
 
        private DateTime? creditCardExp;
        public DateTime? CreditCardExp
        {
            get { return creditCardExp; }
            set { creditCardExp = value; OnPropertyChanged("CreditCardExp"); }
        }
 
        private CardType creditCardType = CardType.All;
        public CardType CreditCardType
        {
            get { return creditCardType; }
        }
        #endregion
 
 
 
        #region INotifyPropertyChanged Members
        void OnPropertyChanged(string name)
        { 
            if(PropertyChanged!=null)
                PropertyChanged(this,new PropertyChangedEventArgs(name));
        }
        public event PropertyChangedEventHandler PropertyChanged;
 
        #endregion
    }

How we need control to show and read it. I like Vista's approach of semi-texboxes. I want to know when my textbox has no text and render it different way. Setters, styles and ControlTemplated, what you think?

<Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="{x:Type m:aTextBox}">
            <Grid Margin="0">
              <Border Name="background"
                              Background="{TemplateBinding Background}"
                              BorderBrush="{TemplateBinding BorderBrush}"
                              BorderThickness="{TemplateBinding BorderThickness}">
                <Decorator x:Name="PART_ContentHost" VerticalAlignment="Top" Margin="0"/>
              </Border>
              <Label Content="{TemplateBinding TextBoxInfo}"
                          x:Name="Message"
                          Foreground="Gray"
                          Opacity="0.9"
                          FontSize="{TemplateBinding FontSize}"
                          FontStyle="Italic"
                          HorizontalAlignment="Left"
                          VerticalAlignment="Top"
                          Margin="0"/>
 
            </Grid>
            <ControlTemplate.Triggers>
              <MultiTrigger>
                <MultiTrigger.Conditions>
                  <Condition Property="HasText" Value="False"/>
                  <Condition Property="IsFocused" Value="True"/>
                </MultiTrigger.Conditions>
              </MultiTrigger>
 
              <MultiTrigger>
                <MultiTrigger.Conditions>
                  <Condition Property="HasText" Value="False"/>
                  <Condition Property="IsFocused" Value="False"/>
                </MultiTrigger.Conditions>
                <Setter TargetName="background" Property="Visibility" Value="Hidden"/>
              </MultiTrigger>
 
              <Trigger Property="HasText" Value="True">
                <Setter TargetName="Message" Property="Visibility"  Value="Hidden"/>
              </Trigger>
 
              <Trigger Property="IsEnabled" Value="false">
                <Setter TargetName="background" Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
                <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/>
              </Trigger>
 
              <Trigger Property="Width" Value="Auto">
                <Setter Property="MinWidth" Value="100"/>
              </Trigger>
 
              <Trigger Property="Height" Value="Auto">
                <Setter Property="MinHeight" Value="20"/>
              </Trigger>
            </ControlTemplate.Triggers>
          </ControlTemplate>
        </Setter.Value>
      </Setter>

 


See, doesn't it looks great?



Yes, this greate, but my country is Enum and I want it binded to Combobox. What to do? This what. We have class named ObjectDataProvider, that can get not only type of object, but it's method as well, so the binding will be really simple

<ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="ctrs">
      <ObjectDataProvider.MethodParameters>
        <x:Type TypeName="m:Countries"/>
      </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
<ComboBox ItemsSource="{Binding Source={StaticResource ctrs}}" IsSynchronizedWithCurrentItem="true"  />

Fine. Our happy use can choose whaever he want and even check out. Let's add some visual effects for controls' transition to make our costomer not just happy, but excited with new WPF technology. I like "scrolling animation". That means, that I have to add animation...


<Storyboard  x:Key="enterBuy">
      <ParallelTimeline>
        <DoubleAnimation
          Storyboard.TargetName="myViewport3D"
          Storyboard.TargetProperty="(Viewport3D.Width)"
          From="{Binding ElementName=myWindow, Path=ActualWidth}"
          To="0"
          AutoReverse="False"
          Duration="0:0:1" />
        <DoubleAnimation
          Storyboard.TargetName="myData"
          Storyboard.TargetProperty="(m:PersonalData.Width)"
          From="0"
          To="{Binding ElementName=myWindow, Path=ActualWidth}"
          AutoReverse="False"
          Duration="0:0:1" />
      </ParallelTimeline>
    </Storyboard>
    <Storyboard  x:Key="exitBuy">
      <ParallelTimeline>
        <DoubleAnimation
          Storyboard.TargetName="myData"
          Storyboard.TargetProperty="(m:PersonalData.Width)"
          From="{Binding ElementName=myWindow, Path=ActualWidth}"
          To="0"
          AutoReverse="False"
          Duration="0:0:1" />
        <DoubleAnimation
          Storyboard.TargetName="myViewport3D"
          Storyboard.TargetProperty="(Viewport3D.Width)"
          From="0"
          To="{Binding ElementName=myWindow, Path=ActualWidth}"
          AutoReverse="False"
          Duration="0:0:1" />
      </ParallelTimeline>
    </Storyboard>

...and when the animation occures

<Window.Triggers>
    <EventTrigger RoutedEvent="Button.Click" SourceName="btnCheckout">
      <EventTrigger.Actions>
        <BeginStoryboard Storyboard="{StaticResource enterBuy}"/>
      </EventTrigger.Actions>
    </EventTrigger>
    <EventTrigger RoutedEvent="Button.Click" SourceName="btnRemove">
      <EventTrigger.Actions>
        <BeginStoryboard Storyboard="{StaticResource exitBuy}"/>
      </EventTrigger.Actions>
    </EventTrigger>
  </Window.Triggers>

Pretty simple, don't you think? The only thing we should do is to connect it to our transactional SAP (why? someone knows for sure) web service (by using WCF, of couse) to fulfill the purchase, add the client's information to SAP CRM and send the items (by using SAP ERP) to our happy customer.


The bottom line. We build almost functional consumer application, that can run either as standalone Windows or as WPF/E control in browser. It has outstanding high-end UI, very functional and easy to use within only a couple of hours. Isn't is really great?


This article source code and executibles

No comments: