Thursday, February 2, 2017

Binding to an indexed property in VB

This is for framework 4.0

We know that we can have indexed properties but how do we bind to them and handle INotifyPropertyChanged? In this post, we will explore binding labels, textboxes, and datagridtextcolumns.

Start a new Visual Basic WPF Application and call it BindingToIndexedProperty.


We will create two labels, two textboxes, and a datagrid with two columns. The labels, textboxes and column headings will be dynamically populated as the application starts. There will also be a button that allows them to be changed to show INotifyPropertyChanged working. We will use an MVVM type model but simplified to use the window's class instead of a separate class.

Let's start with the XAML for the labels and textboxes. Replace the contents of MainWindow.xaml with the XAML below.

<Window x:Class="MainWindow"
        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:BindingToIndexedProperty"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="auto"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="auto"></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"></ColumnDefinition>
            <ColumnDefinition Width="*"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Label Grid.Column="0" Grid.Row="0" Content="{Binding Text[0]}"></Label>
        <Label Grid.Column="0" Grid.Row="1" Content="{Binding Text[1]}"></Label>
        <TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Text[0]}"></TextBox>
        <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Text[1]}"></TextBox>
    </Grid>
</Window>

We see in this XAML that the labels and textboxes are bound to the same indexed property. Note the use of square brackets in the paths.

In MainWindow.xaml.vb add the private and public implementations of the Text property inside the class.

Class MainWindow

    Private _Text(2) As String
    Public Property Text(index) As String
        Get
            Return _Text(index)
        End Get
        Set(value As String)
            If _Text(index) <> value Then
                _Text(index) = value
            End If
        End Set
    End Property

End Class

We can run the application and everything will bind but it won't be very interesting. We can initialize the text in the New method like this.

Public Sub New()
    PreInitialize()
    InitializeComponent()
End Sub

Private Sub PreInitialize()
    Text(0) = "Hello"
    Text(1) = "Goodbye"

End Sub

Running the application now shows the binding to the indexed property has been successful.


Now let's look at implementing INotifyPropertyChanged. Add a button to the XAML with a RoutedCommand. Here is definition of the Routed Command which goes before the opening tag of the Grid.

<Window.Resources>
    <RoutedCommand x:Key="PostInitialize"></RoutedCommand>
</Window.Resources>
<Window.CommandBindings>
    <CommandBinding Command="{StaticResource PostInitialize}" Executed="PostInitialize_Executed"/>
</Window.CommandBindings>

And the button is inserted just before the closing tag for the Grid and looks like this.

<Button Grid.Column="0" Grid.Row="3" Content="Post Initialize" Command="{StaticResource PostInitialize}"/>

We need to add the PostInitialize function to our code like this...

Private Sub PostInitialize_Executed(sender As Object, e As ExecutedRoutedEventArgs)
    Text(0) = "Bonjour"
    Text(1) = "Au Revior"
End Sub

None of this will work until we implement INotifyPropertyChanged. Start by adding an Imports statement...

Imports System.ComponentModel

and an implements clause to the class definition so that it looks like this...

Class MainWindow
    Implements INotifyPropertyChanged

Now that we have said we implement INotifyPropertyChanged we have to add the PropertyChanged event and a method to raise it. We've all seen this code before, I'm sure.

Public Event PropertyChanged(sender As Object, e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
Private Sub NotifyPropertyChanged(Name As String)
    RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(Name))
End Sub

and finally we call NotifyPropertyChanged from the setter of the Text Property. Note we call this function with "Text". Not "Text(0)" or "Text[0]". The setter now looks like this.

Set(value As String)
    If _Text(index) <> value Then
        _Text(index) = value
        NotifyPropertyChanged("Text")
    End If
End Set

Now when we click the [Post Initialize] button the text changes which demonstrates that INotifyPropertyChanged is working.


Now let's see how we could bind the headings of a datagrid to an indexed property. Binding anything other that the Binding property in a DataGridColumn is difficult because they do not exist in the Visual Tree so they don't have a DataContext. Let's start by defining our DataGrid and it's two columns.

Add the following XAML after the <Button... > element

<DataGrid Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2" IsReadOnly="true" AutoGenerateColumns="false">
    <DataGrid.Columns>

        <DataGridTextColumn Header="{Binding Text[0]}" Width="*"></DataGridTextColumn>
        <DataGridTextColumn Header="{Binding Text[1]}" Width="*"></DataGridTextColumn>
    </DataGrid.Columns>
</DataGrid>

If DataGridColumns were in the Visual Tree this would work. But they're not so it doesn't. Give it a try if you don't believe me. There's an old trick using a Proxy Element that is commonly used to overcome this enormous flaw in WPF.

We start by adding the proxy element before the DataGrid like this. It will inherit it's DataContext from the first parent that has a DataContext defined which happens to be the window.

<FrameworkElement x:Name="ProxyElement" Visibility="Collapsed"></FrameworkElement>

In the DataGridColumn's binding we can reference the proxy element using the x:Reference form even though it's not in the same visual tree as the DataGridColumn. Bear in mind that our source is the proxy element so our path is DataContext.<PropertyName>[index]. Our DataGridColumns now looks like this.

<DataGridTextColumn Header="{Binding DataContext.Text[0], Source={x:Reference ProxyElement}}" Width="*"></DataGridTextColumn>
<DataGridTextColumn Header="{Binding DataContext.Text[1], Source={x:Reference ProxyElement}}" Width="*"></DataGridTextColumn>

At this point the editor may highlight the Source clause with an error. However the compiler will not generate an error. This bug has been fixed in Framework 4.5

Note you can now use the same proxy element to bind any dependency property of any DataGridColumn on the page.


Friday, January 6, 2017

Simple Charting

This post refers to Framework 4.0.

I recently decided to write a WPF page that visualizes user logons as both a table and a chart. I could have used some third party charting controls but decided to write my own because I wanted to find out how to do it.

In this example I will generate a simple line chart based on a hard coded tuple of x,y coordinates. The top half of the page will display the chart and the lower half will display the data in a table. There will be a grid splitter that can adjust the height of the two halves of the page and the chart will resize itself when the splitter is adjusted or the page is resized.

The chart is generated by writing lines and text on a canvas. I have broken the rules of MVVM by referencing the canvas control explicitly in background code.

Start a new WPF application and call it LineChart.

Here is the initial XAML that needs to replace the default XAML in MainWindow. If you run it you will see the basic layout of the page.

<Window x:Class="LineChart.MainWindow"
        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:LineChart"
        mc:Ignorable="d"
        Title="Line Chart" Height="500" Width="500"
        DataContext="{Binding RelativeSource={RelativeSource self}}">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"></RowDefinition>
            <RowDefinition Height="8"></RowDefinition>
            <RowDefinition Height="*"></RowDefinition>
        </Grid.RowDefinitions>
        <Canvas Name="ChartCanvas" Grid.Row="0" Background="AliceBlue" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"></Canvas>
        <GridSplitter Grid.Row="1" Width="auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"></GridSplitter>
        <DataGrid Grid.Row="2" AutoGenerateColumns="true" IsReadOnly="true"></DataGrid>
    </Grid>
</Window>

Let's start by making the gridsplitter look a little nicer. I added a small "gripper" image to the application and called it HorizontalGripper. It looks like this...


Add a colored rectangle to grid row 1 and change the GridSplitter to look like this...

        <Rectangle Grid.Row="1" Fill="SteelBlue"></Rectangle>
        <GridSplitter Grid.Row="1" Width="auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
            <GridSplitter.Background>
                <ImageBrush Stretch="None" ImageSource="HorizontalGripper.png"/>
            </GridSplitter.Background>
        </GridSplitter>


Now display the page again and see how the grid splitter stands out.


Now it's time to define our data points and populate the table.

Replace the code behind so that MainWindow.xaml.cs looks like this...

using System.Collections.Generic;
using System.Windows;
using System.Windows.Shapes;
using System.Windows.Media;
using System.Windows.Controls;
using System.Linq;

namespace LineChart
{
    public partial class MainWindow : Window
    {
        public Point[] Points
        {
            get
            {
                return new Point[] { new Point(0,44), new Point (1,10), new Point(2,20), new Point(3,13), new Point(4,44), new Point(5,5), new Point(6,61), new Point(7,16), new Point(8,23), new Point(9,81), new Point(10,55) };
            }
        }

        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

Now we can bind the datagrid to our data points by adding an ItemsSource like this...
<DataGrid Grid.Row="2" AutoGenerateColumns="true" IsReadOnly="true" ItemsSource="{Binding Points}"></DataGrid>

And now our table is populated. Yay table! Now for the real reason we came here -- the chart.


We need to scale the chart to fit in the canvas whenever the canvas size changes. There is a SizeChanged event on the Canvas control so we need to create and register a handler for it. Here is the code to register the handler.

public MainWindow()
{
    InitializeComponent();
    ChartCanvas.SizeChanged += ChartCanvas_SizeChanged;
}

The definition of the SizeChanged event handler starts like this.

void ChartCanvas_SizeChanged(object sender, RoutedEventArgs e) 
{
     double offsetLeft = 50;     // Distance from left of canvas to Y axis and right of canvas to right of X axis
     double offsetBottom = 50;   // Distance from bottom of canvas to X axis and top of canvas to top of Y axis
     SolidColorBrush AxisBrush = new SolidColorBrush(Colors.DodgerBlue);
     SolidColorBrush LineBrush = new SolidColorBrush(Colors.IndianRed);
     double canvasHeight = ChartCanvas.ActualHeight;
     double canvasWidth = ChartCanvas.ActualWidth;

     ChartCanvas.Children.Clear();

     Line XAxis = new Line();
     XAxis.X1 = offsetLeft;
     XAxis.Y1 = canvasHeight - offsetBottom;
     XAxis.X2 = canvasWidth - offsetLeft;
     XAxis.Y2 = canvasHeight - offsetBottom;
     XAxis.Stroke = AxisBrush;
     XAxis.StrokeThickness = 2;
     ChartCanvas.Children.Add(XAxis);

     Line YAxis = new Line();
     YAxis.X1 = offsetLeft;
     YAxis.Y1 = offsetBottom;
     YAxis.X2 = offsetLeft;
     YAxis.Y2 = canvasHeight - offsetBottom;
     YAxis.Stroke = AxisBrush;
     YAxis.StrokeThickness = 2;
     ChartCanvas.Children.Add(YAxis);

 }

At this point we have the X and Y axes displayed and the grid rescales whenever the canvas size changes. Our next task is to display the axis ticks and labels. I have decided to break each axis into ten equal parts so there will be 11 labels on each axis. Before we can add the ticks we need to decide what the range on each axis will be. For simplicity I have decided the range will be zero to the smallest power of ten that is equal to or larger than the largest value on the axis. 

For example, if an axis has a maximum value of 72, the axis range will be 0 to 100 with ticks every 10 units. Note I have not optimized the algebra so the algorithms are clearer.

Add the following code to the end of ChartCanvas_SizeChanged

            double MaxX = Points.Max(p => p.X), XScale = 1;
            double MaxY = Points.Max(p => p.Y), YScale = 1;

            while (MaxX > XScale) { XScale *= 10; }     // X axis will go from 0 to XScale
            while (MaxY > YScale) { YScale *= 10; }     // Y axis will go from 0 to YScale

            double tickLength = 10;

            for (int X = 0; X <= 10; X++)
            {
                Line XTick = new Line();
                XTick.X1 = offsetLeft + (canvasWidth - 2 * offsetLeft) / 10 * X;
                XTick.Y1 = canvasHeight - offsetBottom;
                XTick.X2 = XTick.X1;
                XTick.Y2 = canvasHeight - offsetBottom + tickLength;
                XTick.Stroke = AxisBrush;
                XTick.StrokeThickness = 1;
                ChartCanvas.Children.Add(XTick);
                TextBlock XLabel = new TextBlock();
                XLabel.Text = (X * XScale/ 10).ToString();
                Canvas.SetLeft(XLabel, XTick.X1 - 5);
                Canvas.SetTop(XLabel, XTick.Y2 + 10);
                ChartCanvas.Children.Add(XLabel);
            }

            for (int Y = 0; Y <= 10; Y++)
            {
                Line YTick = new Line();
                YTick.X1 = offsetLeft;
                YTick.Y1 = canvasHeight - offsetBottom - (canvasHeight - 2 * offsetBottom) / 10 * Y;
                YTick.X2 = offsetLeft - tickLength;
                YTick.Y2 = YTick.Y1;
                YTick.Stroke = AxisBrush;
                YTick.StrokeThickness = 1;
                ChartCanvas.Children.Add(YTick);
                TextBlock YLabel = new TextBlock();
                YLabel.Text = (Y * YScale/ 10).ToString();
                Canvas.SetLeft(YLabel, 5);
                Canvas.SetTop(YLabel, YTick.Y1 - 5);
                ChartCanvas.Children.Add(YLabel);
            }

We now have the ticks and labels on both axes. Time to actually plot some data.

            for (int i = 0; i < Points.Length - 1; i++)
            {
                Point point1 = Points[i];
                Point point2 = Points[i + 1];
                Line plotLine = new Line();
                plotLine.X1 = offsetLeft + (canvasWidth - 2 * offsetLeft) / XScale * point1.X;
                plotLine.Y1 = canvasHeight - offsetBottom - (canvasHeight - 2 * offsetBottom) / YScale * point1.Y;
                plotLine.X2 = offsetLeft + (canvasWidth - 2 * offsetLeft) / XScale * point2.X;
                plotLine.Y2 = canvasHeight - offsetBottom - (canvasHeight - 2 * offsetBottom) / YScale * point2.Y;
                plotLine.Stroke = LineBrush;
                plotLine.StrokeThickness = 2;
                ChartCanvas.Children.Add(plotLine);
            }
        


Friday, December 2, 2016

Consuming a WCF service using REST from Angular

I know this doesn't have much to do with WPF but I've been wanting to write RESTful WCF services for some time and consuming them from Angular was plain good fun!

You can click on any of the images below to see full size versions.

We're going to use the AdventureWorks2012 SQL Server sample database from Microsoft.

Start Visual 2015 and start a new WCF Service Application project and call it "products".



At this point Visual Studio has created a sample service called Service1 that contains examples of code. You can delete Service1 and IService1 or simply ignore them.

Our next step is to add our own service called Products. In the solution explorer, right-click on the Products project, select Add and then select New Item.


Select WCF Service and name it "Products". Click [Add]


Now your Solution Explorer looks like this and you can get a clean build.



We will write a Products service that returns a list of products. The first thing to do is to enhance our interface to include a definition of the product class and a definition of the products service. Lets' start with the product class. Open the IProducts class and remove the examples that Visual Studio put there for us. Replace the contents with the following class definition.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;

namespace Products
{
    [DataContract]
    public class Product
    {
        [DataMember]
        public int ID { get; set; }

        [DataMember]
        public string Name { get; set; }

        [DataMember]
        public string ProductNumber { get; set; }

        [DataMember]
        public string Color { get; set; }

        [DataMember]
        public decimal Price { get; set; }
    }
 }

Note the [DataContract] and [DataMember] attributes. They tell WCF how to send this class and its members across the wire to the client.

Now add the interface for the GetProductList method. Note it also has attributes that indicate how we will invoke it (with a GET) and how data will be returned (in Json format). If we wanted to be able to save a product list we would add a similar definition but with Method="POST" and referencing a different method (maybe SaveProductList). The UriTemplate would be the same.

    [ServiceContract]
    public interface IProducts
    {
        [OperationContract]
        [WebInvoke(Method = "GET", ResponseFormat = WebMessageFormat.Json,
            BodyStyle = WebMessageBodyStyle.Bare, UriTemplate = "ProductList/")]
        List<Product> GetProductList();
    }

To recap, this interface converts a request to http://.../ProductList/ using the GET method into a call to GetProductList() and converts the returned data into JSON which is attached to the response. In effect we have a layer that converts REST to SOAP. We will see below that we have the ability to hide or expose the underlying SOAP functionality.

At this point the IProducts.cs file has a "Products" namespace that contains a "Product" class and an IProducts interface.

Before we go any further we need to put our connection string into Web.config so we can access the database. Add the following to your Web.config file inside <configuration>.

  <connectionStrings>
    <add name="localhost" connectionString="Server=localhost;Database=AdventureWorks2012;Trusted_Connection=yes"/>
  </connectionStrings>

While we are here we can define our endpoint behaviors. Merge the following tags into <system.serviceModel> in your Web.config file. Some of these tags will appear to be invalid until they are all in the correct places. Two attributes we could change are httpGetEnabled which determines if SOAP behavior is exposed by the endpoint and includExceptionDetailInFaults which determines if detailed error messages are returned.

    <services>
      <service name="Products.Products" behaviorConfiguration="serviceBehavior">
        <endpoint address="" binding="webHttpBinding" contract="Products.IProducts" behaviorConfiguration="web"/>
      </service>
    </services>

    <behaviors>
      <serviceBehaviors>
        <behavior name="serviceBehavior">
          <serviceMetadata httpGetEnabled="false"/>
          <serviceDebug includeExceptionDetailInFaults="true"/>
        </behavior>
      </serviceBehaviors>
      <endpointBehaviors>
        <behavior name="web">
          <webHttp/>
        </behavior>
      </endpointBehaviors>
    </behaviors>

I am going to use a data extension to convert a datatable to a list of dictionaries which means I need to add a reference to System.Data.DataSetExtensions. In the Solution Explorer right-click on References and select Add Reference.

Scroll to System.Data.DataSetExtensions, click on it to make the checkbox visible, then check the checkbox and click [OK].


Now we can write the products class. Open the Products.svc.cs file. Remove all the example code and replace it with the following. If we had decided to support saving a product list we would write SaveProductList here (after defining the interface in IProducts).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.Data;
using System.Data.SqlClient;

namespace Products
{
    public class Products : IProducts
    {
        public List<Product> GetProductList() 
        {
            String SQL = "SELECT ProductID, Name, ProductNumber, Color, ListPrice FROM Production.Product ORDER BY ProductID";
            String ConnString = System.Configuration.ConfigurationManager.ConnectionStrings["localhost"].ConnectionString;
            DataTable dt = new DataTable();

            using (SqlConnection conn = new SqlConnection(ConnString))
            {
                conn.Open();
                using (SqlCommand comm = new SqlCommand(SQL, conn))
                {
                    using (SqlDataAdapter da = new SqlDataAdapter(comm))
                    {
                        da.Fill(dt);
                    }
                }
            }
            List<Product> ProductList = dt.AsEnumerable().Select(r => new Product()
            {
                ID = r.Field<int>("ProductID"),
                Name = r.Field<String>("Name"),
                ProductNumber = r.Field<String>("ProductNumber"),
                Color = r.Field<String>("Color"),
                Price = r.Field<Decimal>("ListPrice")
            }).ToList();
            return ProductList;
        }

    }
}

We define the GetProductList method that pulls the data from the database and converts it into a list of product objects. The list is automatically converted to JSON by the ResponseFormat = WebMessageFormat.Json clause in the interface's attribute.

At this point you can right-click on Products.svc and select "View in Browser". We disabled SOAP when we set httpGetEnabled="false"  above so we will see the message "Metadata publishing for this service is currently disabled". If we append "/ProductList/" to the URL we will get a JSON file back that represents all the products.

Now it is time to consume the data. Add an html page to the project called "Default" and set it as the start page. It will use some simple Angular to $http.get the JSON and render it in a basic table. Replace the contents of Default.html with this.

<!DOCTYPE html>
<html ng-app="DefaultApp">
<head>
    <title>products</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
    <script>
        angular.module("DefaultApp", [])
        .constant("productsUrl", "/Products.svc/ProductList/")
        .controller("DefaultCtrl",
        function ($scope,$http,productsUrl) {

            $scope.getData = function () {
                $http.get(productsUrl)
                .then
                (
                    function (result) { $scope.products = result.data; }
                    ,
                    function (result) { $scope.statusText = result.statusText; }
                )
            };

            $scope.getData();
        });
    </script>
</head>
<body ng-controller="DefaultCtrl">
    <div>{{statusText}}</div>
    <table>
        <tr ng-repeat="item in products">
            <td>{{item.ID}}</td>
            <td>{{item.Name}}</td>
            <td>{{item.ProductNumber}}</td>
            <td>{{item.Color}}</td>
            <td>{{item.Price | currency}}</td>
        </tr>
    </table>
</body>
</html>

Now run the project. The result will be a table of products.


Wednesday, November 2, 2016

Binding to a DataGrid's SelectedItems property

I have a requirement to only enable a button when there is at least one row selected in a DataGrid. My initial response was to attach the button to a routed command, bind the SelectedItems property of the DataGrid to a collection, and test the collection's count in the CanExecute method. However SelectedItems is not a dependency property for some reason so you cannot bind it in XAML.

We found two solutions to this problem so I will present them both.

1. Create a custom dependency property.

This solution is more work but has the advantage of being easier to consume. I.e. once the work is done, it can be used in many places easily. It is also more 'pure' MVVM.

a. Start by creating a custom class that derives from DataGrid and adds a custom dependency property that can be bound to. Whenever the SelectedItems property changes, the custom dependency property gets changed too.

using System.Windows;
public class CustomDataGrid : DataGrid
{
    public CustomDataGrid()
    {
        this.SelectionChanged += CustomDataGrid_SelectionChanged;
    }

    void CustomDataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        this.SelectedItemsList = this.SelectedItems;
    }

    public IList SelectedItemsList
    {
        get { return (IList)GetValue(SelectedItemsListProperty); }
        set { SetValue(SelectedItemsListProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemsListProperty =
                DependencyProperty.Register("SelectedItemsList", typeof(IList), typeof(CustomDataGrid), new PropertyMetadata(null));
}

b. Now add a "local" reference to the XAML so we can use the custom data grid like this.

<Window x:Class="MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            xmlns:local="clr-namespace:DataGridTesting"
            Title="MainWindow" Height="350" Width="525">
    <Window.CommandBindings>
        <CommandBinding Command="Copy" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"></CommandBinding>
    </Window.CommandBindings>
    <DockPanel>
        <local:CustomDataGrid ItemsSource="{Binding Model}"
                              SelectionMode="Extended"
                              IsReadOnly="True"
                              AutoGenerateColumns="False"
                              SelectedItemsList="{Binding SelectedItemsList, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding}" Width="100" Header="Name"></DataGridTextColumn>
            </DataGrid.Columns>
        </local:CustomDataGrid>
        <Button Content="How Many?" Command="Copy"></Button>
    </DockPanel>

</Window>

c. The code behind looks like this. Note I broke strict MVVM by using a routed command for simplicity. You won't do that, I'm sure.

using System;
using System.Windows;
using System.Windows.Input;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MyViewModel();
    }

    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        if (this.DataContext != null)
            e.CanExecute = (((MyViewModel)this.DataContext).SelectedItemsList.Count > 0);
    }

    private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        MessageBox.Show("You selected " + ((MyViewModel)this.DataContext).SelectedItemsList.Count);
    }
}

d. And here is my ViewModel. Note I used a list of strings so I don't have a Model class. 

using System;
using System.Collections.Generic;

public class MyViewModel
{
    private List<String> _myModel = new List<String> {"Anne","Bob","Connie","David"};

    public IEnumerable<String> Model { get { return _myModel; } }

    private IList _selectedModels = new ArrayList();

    public IList SelectedItemsList
    {
        get { return _selectedModels; }
        set
        {
            _selectedModels = value;
        }
    }
}

2. Passing SelectedItems through the command parameter.

My colleague used the ability to bind the command parameter so that the command passes the DataGrid's SelectedItems property. It isn't perfect MVVM because the command has to bind to the DataGrid using it's name. I'll use Routed Commands to simplify this example too.

a. Start with the XAML this time. We don't need the local reference but we have to name the grid and reference it in the new CommandParameter of the button.

<Window x:Class="MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
            Title="MainWindow" Height="350" Width="525">
    <Window.CommandBindings>
        <CommandBinding Command="Copy" CanExecute="CommandBinding_CanExecute" Executed="CommandBinding_Executed"></CommandBinding>
    </Window.CommandBindings>
    <DockPanel>
        <DataGrid x:Name="AvailableNames"
                              ItemsSource="{Binding Model}"
                              SelectionMode="Extended"
                              IsReadOnly="True"
                              AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding}" Width="100" Header="Name"></DataGridTextColumn>
            </DataGrid.Columns>
        </DataGrid>
        <Button Content="How Many?" Command="Copy" CommandParameter="{Binding ElementName=AvailableNames, Path=SelectedItems}"></Button>
    </DockPanel>
</Window>

b. The code behind looks like this. Note how the CanExecute and Executed methods use the command parameter.

using System;
using System.Windows;
using System.Windows.Input;
using System.Collections;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new MyViewModel();
    }

    private void CommandBinding_CanExecute(object sender, CanExecuteRoutedEventArgs e)
    {
        if (e.Parameter != null)
            e.CanExecute = ((e.Parameter as IList).Count > 0);
    }

    private void CommandBinding_Executed(object sender, ExecutedRoutedEventArgs e)
    {
        MessageBox.Show("You selected " + (e.Parameter as IList).Count);
    }
}

c. Lastly the ViewModel is much simpler now.

using System;
using System.Collections.Generic;

public class MyViewModel
{
    private List<String> _myModel = new List<String> { "Anne", "Bob", "Connie", "David" };

    public IEnumerable<String> Model { get { return _myModel; } }
}


Thursday, October 20, 2016

Two cool techniques

It's been a while since my last post. I guess I haven't been struggling to solve problems like I used to. Today I had a couple of problems to solve that have nothing to do with WPF.

The scenario is that I have log files accumulated on the middle tier and I want to be able to search them. I want to be able to give the user either a list of all log files that match a file name, or a list of log files that match a file name and contain a specified string.

There is a method called GetFiles on the DirectoryInfo class that returns a list of FileInfo objects for files that match a particular filter. I used the LINQ Select method to return the FullName property from each of those FileInfo objects into a collection of strings.

Imports System.Linq
Dim NameFilter as String = "*BOB*"
Dim LogPath as String = "C:\Logs"
Dim ReturnFiles As New Collections.Generic.List(Of String)
ReturnFiles.AddRange(New IO.DirectoryInfo(LogPath).GetFiles(NameFilter).Select(Function(t) t.FullName))

The other requirement is to select files that contain a specific string. Now I could do this by reading and searching each file, but the FindInFiles method is much faster as well as being cooler.

Imports System.Linq
Dim NameFilter as String = "*BOB*"
Dim LogPath as String = "C:\Logs"
Dim ContentFilter as String = "contains"
Dim ReturnFiles As New Collections.Generic.List(Of String)
ReturnFiles.AddRange(Microsoft.VisualBasic.FileIO.FileSystem.FindInFiles(LogPath, ContentFilter, True, Microsoft.VisualBasic.FileIO.SearchOption.SearchTopLevelOnly, NameFilter))

Monday, June 20, 2016

IsSynchronizedWithCurrentItem

I had an interesting problem today. Here's the scenario...

A user is in the search screen and the "Transaction Status" drop down list is set to "All".
The user performs a search, selects a transaction and goes to the edit screen.
The edit screen has a transaction status drop down list too, which is bound to the status of the selected transaction. Let's say the selected transaction has a status of "OPEN".
When the user returns to the search screen the Transaction Status drop down list now displays "OPEN" instead of "All".

Obviously the act of selecting "OPEN" in the edit page's drop down list is selecting "OPEN" in the search page's drop down list. This is commonly achieved by setting both drop down lists' IsSynchronizedWithCurrentItem="true" and sharing the same ItemsSource between both drop down lists.

Both drop down lists are bound to the same ItemsSource to improve performance. When I checked the default style for drop down lists I saw IsSynchronizedWithCurrentItem="true".

    <Style TargetType="{x:Type ComboBox}" x:Key="DropDownList" BasedOn="{StaticResource {x:Type ComboBox}}">
        <Setter Property="IsEditable" Value="False"/>
        <Setter Property="IsSynchronizedWithCurrentItem" Value="True"/>
        <Setter Property="HorizontalAlignment" Value="Stretch"/>
    </Style>

I have three possible solutions.

1. Bind each control to its own ItemsSource. This will slow the application down.
2. Change the default style. This may break something else.
3. Override the style with an explicit attribute.

<ComboBox Name="SearchGLDocumentStatusCombo" DisplayMemberPath="Description" SelectedValuePath="ID" Style="{StaticResource DropDownList}" IsSynchronizedWithCurrentItem="False"/>