. WinForms Form for Data Synchronization and Controls

advertisements

I've been tasked with converting a VB6 Floor Plans application to VB.Net

The application creates Floor Plans using hundreds of dynamically created Images or Shapes placed and manipulated on the Form with X and Y coordinates (.Top, .Left).

For example: a poco data class looks something like this

public class Appliance {
    public int Id      { get; set; }
    public string Name { get; set; }
    public int Top     { get; set; }
    public int Left    { get; set; }
    public int Width   { get; set; }
    public int Height  { get; set; }
    public int Type    { get; set; }
    public int Color   { get; set; }
    public bool Visible{ get; set; }
}

I started working on a FloorPlan class that contains lists of POCO objects like that which will represent the images or shapes and their positions on the form. After sketching out the following, I realized I must be doing it all wrong.

// Populate DATA objects from DBFiles
List<Appliance>  appliancesData = GetAppliancesFromDataFile()
List<PictureBox> appliancesUI   = new List<PictureBox>();

// create a bunch of PictureBox controls
foreach (var appliance in appliances){
  Image img = GetApplianceImage(appliance);
  Appliances.Add(new PictureBox { .Image = img })
  appliancesUI.Controls.Add(img);
}

// Add those PictureBox controls to the Form (via Panel)
foreach (var pic in appliancesUI){
  FormPanel.Controls.Add(pic);
}

I know there HAS to be a better way to do this. I need a link between the Raw Data in my classes to actual Image Controls added to the Form. There may not be a way to have 2way data-binding, but theres gotta be something better than this without deriving the poco classes from PictureBox controls.

What's the best way to sync the data between my data in my poco classes and the properties of the Form Image objects that will be created and added to the form and stop this madness?


Since you're new to WPF, I'm going to go step by step on this, therefore it can get a little long and sometimes repeating, but bear with me.

Note: First of all, WPF might look like a complex, intimidating framework when you start looking into it, but it's not. It's actually really simple once you get to the realization that UI is not Data and start thinking all UI functionality in terms of DataBinding-based interactions between your UI components and your Data Model.

There's a very good series of articles by Reed Copsey, Jr. about the mindshift needed when moving from winforms to WPF. There's also a much shorter article by Rachel Lim I always point people to that gives a nice overview of the WPF mentality.

Step 1:

So, Let's use your Appliance class as a starting point for this:

public class Appliance
{
    public int Id         { get; set; }
    public string Name    { get; set; }
    public double Top     { get; set; }
    public double Left    { get; set; }
    public double Width   { get; set; }
    public double Height  { get; set; }
    public int Type       { get; set; }
    public string Color   { get; set; }
    public bool Visible   { get; set; }
}

Notice that I used double instead of int for size and position, because WPF actually uses doubles since those are device-independent units rather than pixels, and string for Color because it simplifies the example (we can actually use "Red", "Green", and so on).

Step 2:

So, the very first thing we need here in terms of UI, is a piece of UI that will "understand" it needs to take a List or Collection of our Appliance class and put a UI element on the screen for each item in the collection. Fortunately WPF provides that right out of the box, via the ItemsControl class.

Assuming we just created our project in Visual Studio using File -> New Project -> WPF Application, this is the default XAML for MainWindow:

<Window x:Class="FloorPlan.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>
</Window>

We'll get rid of the <Grid></Grid> part since we don't need that, and replace it by our ItemsControl:

<ItemsControl ItemsSource="{Binding}"/>

Notice that I'm Binding the ItemsSource property. WPF is going to resolve this simple Binding to whatever the DataContext of the ItemsControl is, therefore we will assign this in code behind (by now):

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        //Let's assign our DataContext here:
        this.DataContext = new List<Appliance>
        {
            new Appliance() {Top = 20, Left = 40, Width = 30, Height = 30, Color = "Red"},
            new Appliance() {Top = 100, Left = 20, Width = 80, Height = 20, Color = "Blue"},
            new Appliance() {Top = 60, Left = 40, Width = 50, Height = 30, Color = "Green"}
        };
    }
}

Notice that we're actually setting the Window's DataContext, rather than the ItemsControl, but thanks to WPF's Property Value Inheritance, the value of this property (and many others) is propagated down the Visual Tree.

Let's run our project to see what we have so far:

Not much is it? Don't worry, we're just getting started. The important thing to note here is that there's 3 "things" in our UI, which correspond to the 3 items in the List<Appliance> in our DataContext.

Step 3:

By default, the ItemsControl will stack elements vertically, which isn't what we want. Instead, we're going to change the ItemsPanel from the default StackPanel to a Canvas, like this:

<ItemsControl ItemsSource="{Binding}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ItemsControl>

And then, in order to have each UI element properly positioned and sized, we're going to Style the Item Containers so that they will take the values from the Top, Left, Width, and Height properties from the Appliance class:

<ItemsControl ItemsSource="{Binding}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>       

    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="Canvas.Left" Value="{Binding Left}"/>
            <Setter Property="Canvas.Top" Value="{Binding Top}"/>
            <Setter Property="Width" Value="{Binding Width}"/>
            <Setter Property="Height" Value="{Binding Height}"/>
        </Style>
    </ItemsControl.ItemContainerStyle>
</ItemsControl>

Notice that we're binding Canvas.Left and Canvas.Top as opposed to just Left and Top, because the WPF UI elements don't have a Top and Left properties themselves, but rather these are Attached Properties defined in the Canvas class.

Our project now looks like this:

Step 4:

Now we got sizing and positioning right, but we still get this ugly text instead of a proper visual for our Appliances. This is where Data Templating comes into play:

<ItemsControl ItemsSource="{Binding}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="Canvas.Left" Value="{Binding Left}"/>
            <Setter Property="Canvas.Top" Value="{Binding Top}"/>
            <Setter Property="Width" Value="{Binding Width}"/>
            <Setter Property="Height" Value="{Binding Height}"/>
        </Style>
    </ItemsControl.ItemContainerStyle>

    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Border Background="{Binding Color}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

By setting the ItemsControl.ItemTemplate we define how we want each item in the List<Appliance> to be visually represented.

Notice that I'm binding the Border.Background property to the Color property, which is defined in the Appliance class. This is possible because WPF sets the DataContext of each visual item in the ItemsControl to it's corresponding Data Item in the List<Appliance>.

This is our resulting UI:

Starting to look better, right? Notice we didn't even write a single line of code so far, this has all been done with just 20 lines of declarative XAML and DataBinding.

Next Steps:

So, now we have the basics in place, and we have achieved the goal of keeping the Appliance class completely decoupled from the UI, but still there are a lot of features that we may want to include in our app:

  • Selecting items by clicking on them: this can be achieved by changing the ItemsControl to a ListBox (while leaving its properties intact) just by using Ctrl+H. Since ListBox actually derives from ItemsControl, we can use the XAML we wrote so far.
  • Enable click-and-drag: This can be achieved by putting a Thumb control inside the DataTemplate and handling its DragDelta event.
  • Enable two-way DataBinding: This will allow you to modify the properties in the Appliance class and have WPF automatically reflect the changes in the UI.
  • Editing items' properties: We might want to create an edition panel where we put TextBoxes and other controls that will allow us to modify the properties of each appliance.
  • Add support for multiple types of objects: For our app to be complete, it will need different object types and their respective visual representations.

For examples of how to implement these features, see my Lines Editor and Nodes Editor samples.

I think this will be a good starting point for your app, and a good introduction to WPF as well. It's important that you take some time to read all the linked material to get a solid understanding of the concepts and underlying APIs and features that we're using here. Let me know if you need further help or post a new question if you need to implement any of the extra features.

I think I don't even need to mention the fact that implementing all this in winforms would be significantly more tedious, with lots of code behind and much more work and an inferior result.