Friday, January 9, 2015

WPF Master-Detail sync

today I dived into WPF data binding and checked how to create an easy master-detail view. This following sample show my findings (description below):

XAML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<Window x:Class="MasterDetail.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Height="350"
        Width="525" 
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="1*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="118" />
            <ColumnDefinition Width="1*" />
        </Grid.ColumnDefinitions>
        <ListView Grid.Column="0"
                  Grid.Row="0"
                  Margin="5"
                  ItemsSource="{Binding PersonList}" IsSynchronizedWithCurrentItem="True"
                  SelectedIndex="0">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Name"
                                    DisplayMemberBinding="{Binding Name}"
                                    Width="Auto" />
                </GridView> 
            </ListView.View>
        </ListView>
        <ListView Grid.Column="1"
                  Grid.Row="0"
                  ItemsSource="{Binding PersonList/OrderList}"
                  IsSynchronizedWithCurrentItem="True"
                  Margin="5">
            <ListView.View>
                <GridView>
                    <GridViewColumn Header="Name"
                                    DisplayMemberBinding="{Binding Name}"
                                    Width="Auto" />
                </GridView>
            </ListView.View>
        </ListView>
        <Button Grid.Column="1"
                Grid.Row="1"
                Width="100"
                Height="25"
                HorizontalAlignment="Right"
                Margin="0,5,5,5">Close</Button>
    </Grid>
</Window>

Code behind:
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace MasterDetail
{
    /// <summary>
    /// Interaktionslogik für MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public class Person : INotifyPropertyChanged //,IEditableObject
        {
            #region Name

            private string name;
            public string Name
            {
                get
                {
                    return name;
                }
                set
                {
                    name = value;
                    RaisePropertyChanged();
                }
            }
            
            #endregion

            public ObservableCollection<Order> OrderList { get; set; }
            public Person(string name, Order[] list)
            {
                this.Name = name;
                this.OrderList = new ObservableCollection<Order>();
                list.ToList().ForEach(x => this.OrderList.Add(x));
            }

            public event PropertyChangedEventHandler PropertyChanged;
            private void RaisePropertyChanged([CallerMemberName]string prop = null)
            {
                if(this.PropertyChanged != null)
                {
                    this.PropertyChanged(this, new PropertyChangedEventArgs(prop));
                }
            }
        }

        public class Order
        {
            public string Name { get; set; }
            public Order(string name)
            {
                this.Name = name;
            }
        }

        public ObservableCollection<Person> PersonList { get; set; }
        public MainWindow()
        {
            PersonList = new ObservableCollection<Person>();
            
            #region set data
            
            PersonList.Add(new Person("Peter Parker", 
                           new Order[]{
                                new Order("spiderman costume"),
                                new Order("comics"),
                                new Order("science books")
                           }));

            PersonList.Add(new Person("Tony Stark",
                           new Order[]{
                                new Order("screw driver"),
                                new Order("tie"),
                                new Order("headset"),
                                new Order("Mobile phone")
                           }));

            PersonList.Add(new Person("Bruce Benner",
                           new Order[]{
                               new Order("shorts")
                           }));

            #endregion

            InitializeComponent();
        }
    }
}

Findings:

  • Setting the datacontext to the codebehind is a bit tricky see line 7 in XAML
  • IsSynchronizedWithCurrentItem makes sense if you use more than 1 control to display a collection. The control is not directly using the list, but it wraps it using a "default" listcollectionview, which has a current item. This property can be used to sync the positions of the different controls. A problem here is, that listcollectionview is a wpf class, which should not be used inside the viewmodel so getting the current item can be a bit tricky and possibly needs to be done in a view-service, which returns a reference to the current item.
  • line 32 is the highlight in this post. First I tried the same with a dot ("."), but there the output window of visual studio told me (eligible) that there is no property of the given name for the observablecollection. What does the trick is to use the current item, which will be requested by using the slash ("/"). So binding to /OrderList binds to the order list of the current item. It is different behavior to a collection-view which automatically binds to the current item.
kind regards, Daniel

Thursday, January 8, 2015

WPF Zooming

... in response to MarkLTX, who posted an awesome article about zooming and how double-animation makes the user-experience much better ( http://www.codeproject.com/Tips/860914/Add-Zooming-to-a-WPF-Window-or-User-Control ):

In addition to the article I would like to mention a method which wasn't described. I think it is the most natural way in a WPF application to use XAML-Binding (without any code behind) and to use the slider only, instead of using an animation as e.g.: Word 2013 does it (maybe they also have an animation to keep things smooth, but if so, I don't really recognize it). Anyway, the animation was added to give the user a better feeling about what is going on and to show him (through the animation) that the current action which will be proceed is "zooming" and not any magic which confuses him.

I think that the user-activity of changing a slider makes it unnecessary to do any other things (as long as the performance of the application is OK).

Here a sample application which uses a slider to zoom:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<Window x:Name="window"
        x:Class="ZoomerApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Height="350"
        Width="525">

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="1*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <WrapPanel Margin="10,10,10,0">
            <WrapPanel.LayoutTransform>
                <TransformGroup>
                    <ScaleTransform ScaleX="{Binding Value, ElementName=ZoomSlider}"
                                    ScaleY="{Binding Value, ElementName=ZoomSlider}" />
                    <SkewTransform />
                    <RotateTransform />
                    <TranslateTransform />
                </TransformGroup>
            </WrapPanel.LayoutTransform>
                <Button>Hello</Button>
                <Button>Hello</Button>
                <Button>Hello</Button>
                <Button>Hello</Button>
                <Button>Hello</Button>
                <Button>Hello</Button>
                <Button>Hello</Button>
                <Button>Hello</Button>
                <Button>Hello</Button>

        </WrapPanel>
        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="1*" />
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Slider x:Name="ZoomSlider"
                    Width="100"
                    HorizontalAlignment="Right"
                    Margin="10,2,10,0"
                    Grid.Column="0"
                    Value="1" />
            <StackPanel Orientation="Horizontal"
                        Grid.Column="1">
                <Label Content="{Binding Value, ElementName=ZoomSlider}"
                       ContentStringFormat="{}Zooming: {0:N2}" />
            </StackPanel>
            <Button Width="100"
                    Content="OK"
                    Margin="20,2,0,10"
                    Height="25"
                    Grid.Column="3"
                    IsDefault="True" />
            <Button Width="100"
                    Margin="2,2,10,10"
                    Content="Cancel"
                    Height="25"
                    Grid.Column="4"
                    IsCancel="True" />
        </Grid>
    </Grid>
</Window>


What I did:
  • I created a window with a main grid and 2 containers inside...
    • the top container is a WrapPanel which will be zoomed
    • the bottom container has a slider which sets the zoom level
  • The binding is on the slider.value (so I named the slider ZoomSlider to be able to call it by its element-name). There are much more elegant ways, but I think this one is elegant enough for this quick sample.
  • line 18 and 19 are the most important
    • a binding is set up on the value of the slider
    • the value is set on line 49 to default 1 what equals 100%
  • line 52 creates a label which visualizes the content of the slider.value.
I used to create zooming like this and always got good feedback about it (I think the main reason is because it behaves so similar to the word-solution). What also makes this solution attractive for the developer is that no code in the code-behind is needed to accomplish zooming (as long as the zooming level should not be persisted).

kind regards, Daniel