How to play the Accordion - WPF Toolkit

The accordion is a musical instrument invented in Europe in the beginning of the 19th century. It produces music (or rather noise) by expanding and collapsing it while pressing buttons. This metaphor is applied to software: you may know the Accordion user interface control from Microsoft Outlook's navigation bar. In the last couple of years the control appeared in an ASP.NET AJAX implementation and a SilverLight implementation. Recently, the latter was ported to WPF and published as part of the February 2010 release of the WPF Toolkit. This article describes how to use this great WPF control.

Introduction

The Accordion is an ItemsControl that allows you to provide multiple panes and expand them one at a time (well, by default). The items shown are instances of AccordionItem. Clicking on a header will expand (actually 'select') or collapse the item's content. The Accordion class carries the usual responsibilities of an ItemsControl, like providing templates for header and content, and managing the selected items (plural, that is). On top of that, it supports the following extra properties:

  • SelectionMode: One, OneOrMore, ZeroOrOne, or ZeroOrMore
  • ExpandDirection: Left, Right, Up, or Down
  • SelectionSequence: Simultaneous or CollapseBeforeExpand [I'm pretty sure that this one doesn't work in the WPF version]

Hello World

Let's go for a first test-drive. Download the toolkit, create a new WPF project, and add the following two references:

Register the namespace in your xaml:

xmlns:layoutToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Layout.Toolkit"

 

Then drop the following code in the window:

<layoutToolkit:Accordion>

    <layoutToolkit:AccordionItem Header="Red">

        <Rectangle Fill="Red" Height="120" Width="200" />

    </layoutToolkit:AccordionItem>

    <layoutToolkit:AccordionItem Header="Orange">

        <Rectangle Fill="Orange" Height="120" Width="200" />

    </layoutToolkit:AccordionItem>

    <layoutToolkit:AccordionItem Header="Yellow">

        <Rectangle Fill="Yellow" Height="120" Width="200" />

    </layoutToolkit:AccordionItem>

    <layoutToolkit:AccordionItem Header="Green">

        <Rectangle Fill="Green" Height="120" Width="200" />

    </layoutToolkit:AccordionItem>

    <layoutToolkit:AccordionItem Header="Blue">

        <Rectangle Fill="Blue" Height="120" Width="200" />

    </layoutToolkit:AccordionItem>

    <layoutToolkit:AccordionItem Header="Indigo">

        <Rectangle Fill="Indigo" Height="120" Width="200" />

    </layoutToolkit:AccordionItem>

    <layoutToolkit:AccordionItem Header="Violet">

        <Rectangle Fill="Violet" Height="120" Width="200" />

    </layoutToolkit:AccordionItem>

</layoutToolkit:Accordion>

 

You end up with sir Isaac Newton's 7 colors of the rainbow:

The ExpandDirection property -unsurprisingly- changes the direction in which the items expand. Under the hood, a rotation transformation is applied to the header, so be careful when using images. This code:

<layoutToolkit:Accordion ExpandDirection="Right">

 

will transform our accordion like this:

In some applications you may want to open an item just by hovering over its header, instead of clicking. This behavior doesn't come out of the box, but it's easy to implement: just register a handler for the MouseEnter event for each AccordionItem:

C#

private void AccordionItem_MouseEnter(object sender, MouseEventArgs e)

{

    AccordionItem item = sender as AccordionItem;

    item.IsSelected = true;

}

 

XAML

<layoutToolkit:Accordion ExpandDirection="Right">

    <layoutToolkit:Accordion.Resources>

        <Style TargetType="{x:Type layoutToolkit:AccordionItem}">

            <EventSetter Event="MouseEnter" Handler="AccordionItem_MouseEnter" />

        </Style>

    </layoutToolkit:Accordion.Resources>

    <layoutToolkit:AccordionItem Header="Red">

 

Deeper dive

I decided to build a test application (or torture chamber) to figure out whether or not the control is ready to be used in a production application. An Accordion is used a main navigation control, displaying more than just rectangles: a TextBox, a picture library inspired by the rainbow accordion, a control with a variable height (a TreeView), and a control that behaves badly in any WPF application (the WPF WebBrowser is just a thin wrapper around the IE ActiveX). Here are a couple of screenshots:

Sizing

In their default style, the accordion and its items only occupy the space they need, so smaller content also shrinks the headers:

That's why the main navigation accordion has its HorizontalAlignment set to Stretch. While the width of the main navigation accordion is fixed, its height will remain unpredictable - specifically if multiple items can be selected/expanded. A good old scrollbar will do:

By the way, the size of an item doesn't change dynamically. In our sample application, the expansion of treeview items will just add a scrollbar, not push down the accordion's headers:

Only when the same item is unselected and then reselected, the new height is applied:

The picture library needs a more predictable behavior (fixed height and width), otherwise you end up with unacceptable looks - like a shrunk accordion, or headers being pushed out of sight:

Here's how to deal with this:

<Style TargetType="Image">

    <Setter Property="Stretch" Value="UniformToFill" />

    <Setter Property="Height" Value="280" />

    <Setter Property="Width" Value="400" />

</Style>

Styling

The application is styled through the very popular WPF Themes, yet another port from SilverLight. Unfortunately the libraries don't contain styles for Toolkit controls (yet). The default styles for the Accordion are not really innovative - the selected item gets the same style as the selected date in a Calendar control:

So download the WPF Toolkit's source code, and copy-paste the relevant styles into a resource dictionary:

These are the relevant styles:

  • AccordionButton: displays the animated arrow in the header,
  • ExpandableContentControl: displays the item's content,
  • Accordion: displays the container,
  • AccordionItem: displays the item, basically a 2x2 grid, and
  • TransitioningContentControl: starts the animation when content changes.

Now you have access to the whole look and feel of the control. Just don't forget to register the resource dictionary in your XAML:

<Grid.Resources>

    <ResourceDictionary Source="Themes\CustomAccordion.xaml" />

</Grid.Resources>

 

Source Code

Here's the full source code of the sample project: U2UConsult.WPFToolkit.Accordion.Sample.zip (1,98 mb).

Enjoy !


The Missing Linq to SQL Spatial

This article provides hints and hacks on how to use the SQL Server spatial data types -Geography and Geometry- in Linq to SQL. The data provider has a problem with serializing and deserializing the SQL UDT's. If you ever tried to use these data types in a Linq to SQL (or Entity Framework) project then you certainly encountered the following error: “One or more selected items contain a data type that is not supported by the designer”.

  • Setting up a test environment

I created a small database called 'Spatial' with a schema called 'Europe', and a table called 'Countries'. Here's the structure of the table:

CREATE TABLE [Europe].[Countries](

    [CountryID] [int] IDENTITY(1,1) NOT NULL,

    [CountryName] [nvarchar](50) NOT NULL,

    [Shape] [geography] NOT NULL,

 CONSTRAINT [PK_Countries] PRIMARY KEY CLUSTERED

(

    [CountryID] ASC

)


The source code that accompanies this article contains the scripts to create the assets and populate the table. Here's an indication of the table contents:

I created a Visual Studio 2008 Console application, and added a reference to Microsoft.SqlServer.Types. Then I added Linq to SQL classes (Europe.dbml). When I dropped the Europe.Counties table into the designer, the result was the expected error message:

  • Hint 1: Use views that cast the data type

I decided to create a view in the schema: vw_Countries. It exposes the same signature as the table, but with one difference: the shape column is returned as Varbinary(MAX) instead of Geography:

CREATE VIEW [Europe].[vw_Countries]

AS

SELECT CountryID

      ,CountryName

      ,CONVERT(VARBINARY(MAX), Shape) AS Shape

FROM Europe.Countries

 

The Geography UDT is physically stored as Varbinary(MAX) in SQL Server, so the table and the view are basically the same. There's one important difference though: Visual Studio is not allergic to the view. You can easily drag and drop it into the data model designer:

The Linq data provider delivers the varbinary data as System.Data.Linq.Binary, but is -strangly enough- unable to convert this back to SqlGeography. It took me a while to figure out the conversion myself, but here's a working version:

Binary belgiumb = (from c in db.vw_Countries

                   where c.CountryID == 4

                   select c.Shape).FirstOrDefault();

SqlGeography geo = new SqlGeography();

geo.Read(new System.IO.BinaryReader(new System.IO.MemoryStream(belgiumb.ToArray())));

 

  • Hint 2: Package your conversion code

We're going to get stuck with this type of conversions for a while, so it makes sense to wrap the calculations e.g. in extension methods. Here's an example:

/// <summary>

/// Converts a Linq Binary to a SQL Server Geograpy.

/// </summary>

/// <remarks>Throws an Exception if the Binary contains invalid data.</remarks>

public static SqlGeography AsGeography(this Binary binary)

{

    if (binary == null)

    {

        return SqlGeography.Null;

    }

 

    SqlGeography result = new SqlGeography();

 

    result.Read(

        new System.IO.BinaryReader(

            new System.IO.MemoryStream(

                binary.ToArray())));

 

    return result;

}

 

So now I can call it like this:

SqlGeography b = (from c in db.vw_Countries

                  where c.CountryID == 4

                  select c.Shape).FirstOrDefault().AsGeography();

Console.WriteLine(" The area of Belgium is {0} m²", b.STArea().Value);

 

  • Hack 1: Change source mappings

I wanted to execute my queries directly against the table. So I copy/pasted the view in the dbml, and modified the source mapping:

Now I can call the queries like this:

belgium =

    SqlGeography.Parse((from c in db.Countries

                        where c.CountryID == 4

                        select c.Shape.ToString()).FirstOrDefault());

 

  • Hack 2: Change data type mappings

Since I wanted IntelliSense, I copy/pasted the Country table again, and changed the .NET type of the Shape property to SqlGeography:

The good news is: I now have IntelliSense at design time:

The bad news is: I will have exceptions all over the place.

If you return a spatial data type in the select, then a query is successfully constructed and sent to SQL Server, but the return values cannot be deserialized. Here's such a query:

belgium = (from c in db.Countries2

           where c.CountryID == 4

           select c.Shape).FirstOrDefault();

 

It results in a runtime exception:

If you call a method -apart from ToString()- on a spatial data type in the where-clause, then you'll bump into a NotSupportedException already at compile time. Here's such a query:

var query = (from c in db.Countries2

             where (c.Shape.STArea() > 10).Value

             select c.CountryName).FirstOrDefault();

 

And its result at compile time:

  • Intermezzo

I tried to use Varbinary as data type for the _Shape backing variable, and SqlGeography as data type for the Shape property, and call conversions in the getter and setter. It didn't work: the getters and setters seem to be bypassed by the Linq provider.

Conclusion: hack 2 was not a good idea. We'll stop using the Countries2 entity...

  • Hint 3: Use scalar functions

Since STIntersects cannot be used, I created a scalar function that takes two Well-Known Texts, and returns whether or not the shapes intersect:

CREATE FUNCTION [Europe].[Intersects]

(

    @Shape1 NVarchar(MAX),

    @Shape2 NVarchar(MAX)

)

RETURNS integer

AS

BEGIN

 

    DECLARE @Geo1 Geography = geography::Parse(@Shape1)

    DECLARE @Geo2 Geography = geography::Parse(@Shape2)

 

    RETURN @Geo1.STIntersects(@Geo2)

 

END

 

Again, Visual Studio's designer has no problem with this, so you can drag and drop the function into the DataContext:

So you can use it in your queries:

var query = from c in db.Countries

            where db.Intersects(c.Shape.ToString(), belgium.ToString()) == 1

            select c.CountryName;

 

The response time seems to be good enough against such a small table. But I know that it is never a good idea to call functions in the where-clause of a query. It will force a table scan and an evaluation of the function for each row in the table. If you can live with the performance, then I suggest you just stick to this type of functions: they're highly reusable since they don't contain any hard-code table names.

  • Hint 4: Use table valued functions

If your geographical tables get bigger, it makes sense to define table valued functions or stored procedures to do the heavy lifting. The following example doesn't mask the usage of STIntersects in the where-clause, and may let SQL Server decide to use a spatial index (or you may even use a hint):

CREATE FUNCTION [Europe].[Intersectors]

(   

    @Shape NVarchar(MAX)

)

RETURNS TABLE

AS

RETURN

(

    SELECT CountryID, CountryName

    FROM Europe.Countries

    WHERE Shape.STIntersects(geography::Parse(@Shape)) = 1

)

 

And yes: I do realize that Intersectors probable doesn't appear in a regular dictionary...

Here's how a call to it looks like in Linq - we still have IntelliSense:

var query = from c in db.Intersectors(belgium.ToString())

             select c.CountryName;

 

I couldn't keep myself from comparing the performance of the two functions. When I checked the actual query plans,my jaw hit the ground Surprised, ouch! Completely against my expectation, the cost of the table value function was higher than the cost of the scalar function. And not just a little bit, but 5 times higher:

Anyway, you should never blindly base your conclusions on just the query plans, so I started the SQL Profiler to inspect reality. After thorough testing I was relieved Tongue out. I observed that the tabled valued function easily outruns the scalar version: it consumes between 3 and 8 times less CPU and returns the results 5 to 10 times faster. This is actually an impressive difference for such a small table. Here's the result from a test session (scripts are included in the source code):

A negative aspect of this table valued function is its reusability: the function only works against the Europe.Countries table. If you need to query more tables with spatial data, then you need to add complexity to it, or start copy/pasting.

  • Conclusions

The current Linq to Sql provider doesn't support the SQL Server spatial data types properly, so you have to be creative if you want or need to use these. It's not a waste of time to invest some effort in implementing the work arounds I suggested, and in optimizing and performance tuning these. Your solution will last for a while, since there seems to be no Microsoft solution on the horizon. The problems are not solved in SQL 2008 R2, Visual Studio 2010, or .NET 4.0.

  • Source code

Here's the source code for the test program. It also contains the necessary SQL scripts for creating and populating the entities, as well as for testing and performance tuning: U2UConsult.DockOfTheBay.LinqToSpatialSample.zip (153,76 kb)

Here's how its output should look like:

  • Credits

I would like to thank my colleague Kris Vandermotten for his input during the research.


Converting Spatial Coordinates with Proj.NET

In my previous article I expressed some disappointment in the usefulness of the Map Projections in SQL Spatial Tools on CodePlex. There's not much you can do with these in a real-life application. Fortunately there's also Proj.NET on CodePlex, a flexible advanced point-to-point coordinate conversion engine that is used internally by a lot of open source GIS projects.

Here's a small fraction of its object model:

Geographic Coordinate Systems 

The only predefined geographic coordinate system is WGS84:

ICoordinateSystem gcs_WGS84 = GeographicCoordinateSystem.WGS84;

 

But you can easily create your own, from Well-Known Text (WKT) or through the object model:

string wkt_WGS84 =

    "GEOGCS[\"GCS_WGS_1984\"," +

         "DATUM[\"D_WGS_1984\",SPHEROID[\"WGS_1984\",6378137,298.257223563]]," +

         "PRIMEM[\"Greenwich\",0]," +

         "UNIT[\"Degree\",0.0174532925199433]" +

    "]";

ICoordinateSystem gcs_WGS84 = CoordinateSystemWktReader.Parse(wkt_WGS84) as ICoordinateSystem

Projected Coordinate Systems

The only predefined projected coordinate system is UTM:

IProjectedCoordinateSystem pcs_UTM31N = ProjectedCoordinateSystem.WGS84_UTM(31, true);

 

But again, you can easily create your own, from WKT or through an object model. Here's how Lambert 2008 -a local Belgian projection- looks like:

string wkt_Lam08 =

    "PROJCS[\"ETRS89 / Belgian Lambert 2008\"," +

        "GEOGCS[\"ETRS89\"," +

            "DATUM[\"European Terrestrial Reference System 1989\"," +

                "SPHEROID[\"GRS 1980\",6378137.0,298.257222101," +

                    "AUTHORITY[\"EPSG\",\"7019\"]]," +

                "TOWGS84[0.0,0.0,0.0,0.0,0.0,0.0,0.0]," +

                "AUTHORITY[\"EPSG\",\"6258\"]]," +

            "PRIMEM[\"Greenwich\",0.0," +

                "AUTHORITY[\"EPSG\",\"8901\"]]," +

            "UNIT[\"degree\",0.017453292519943295]," +

            "AXIS[\"Geodetic latitude\",NORTH]," +

            "AXIS[\"Geodetic longitude\",EAST]," +

            "AUTHORITY[\"EPSG\",\"4258\"]]," +

        "PROJECTION[\"Lambert Conic Conformal (2SP)\"]," +

        "PARAMETER[\"central_meridian\",4.359215833333335]," +

        "PARAMETER[\"latitude_of_origin\",50.79781500000001]," +

        "PARAMETER[\"standard_parallel_1\",49.833333333333336]," +

        "PARAMETER[\"false_easting\",649328.0]," +

        "PARAMETER[\"false_northing\",665262.0]," +

        "PARAMETER[\"standard_parallel_2\",51.16666666666667]," +

        "UNIT[\"m\",1.0]," +

        "AXIS[\"Easting\",EAST]," +

        "AXIS[\"Northing\",NORTH]," +

        "AUTHORITY[\"EPSG\",\"3812\"]]";

IProjectedCoordinateSystem pcs_Lam08 =

    CoordinateSystemWktReader.Parse(wkt_Lam08) as IProjectedCoordinateSystem;

 

The list of supported projections includes Mercator, Transverse Mercator, Albers, Lambert Conformal, and Krovak.

If you download the project's source code, you'll also discover an SRIDReader class that allows you to instantiate a coordinate system from nothing more than its Spatial Reference ID.

Transformations

A coordinate conversion can be defined and called through the API as follows:

CoordinateTransformationFactory ctfac = new CoordinateTransformationFactory();

ICoordinateTransformation trans = ctfac.CreateFromCoordinateSystems(gcs_WGS84, pcs_UTM31N);

double[] fromPoint = new double[] { 4.296545, 50.880324 };  // U2U Consult Head Office, in degrees

double[] toPoint = trans.MathTransform.Transform(fromPoint);

 

If you're lucky, then you also get the inverse transformation, but it's not always implemented.

try

{

    IMathTransform inversedTransform = trans.MathTransform.Inverse();

    double[] point = inversedTransform.Transform(toPoint);

}

catch (NotImplementedException ex)

{

    // Your exception handling here...

}

 Test Client

Here are U2U Consult's Headquarter's coordinates:

I built a small test client that applies some transformations on these (WGS 84 - UTM - Lambert 1972 - Lambert 2008). Here's how the result looks like:

For the sake of completeness: here's the full source code: ProjNetClient.zip (38,93 kb)


SQL Spatial Tools: Map Projections

SQL Server Spatial Tools on CodePlex contains useful extra functions for the SqlGeometry and SqlGeography data types, as well as a new data type for affine transformations (to scale, translate, and rotate) and a handful of Map Projections. This article describes how to use these projections and visualize the result in Windows Presentation Foundation.

All projections are instantiated from static method calls against the SqlProjection class, with one to five parameters. SQL Spatial Tools contains sample T-Sql scripts, but here's how it looks like in C# (for the inverse projection, don't forget to first assign a Spatial Reference System Identifier to the geometry):

Projection

SqlGeography shape3D = new SqlGeography();

shape3D = SqlGeography.Parse("some valid WKT"); // Or read from DB

SqlProjection proj = SqlProjection.LambertConformalConic(0, 90, 12, 36);

SqlGeometry shape2D = proj.Project(shape3D);

Inverse Projection

SqlGeometry shape2D = SqlGeometry.Parse("some valid WKT");

shape2D.STSrid = 4326; // WGS 84

SqlProjection proj = SqlProjection.AlbersEqualArea(0, 0, 15, 30);

SqlGeography shape3D = proj.Unproject(shape2D);

 

That's all there is to!

 

I added this code to an improved version of my SqlGeometry Extension Methods for WPF Visualization. Here's how the resulting application looks like, displaying a reduced shape of Belgium, and the exact location of my office @ U2U Consult: 

 

OK, I admit: projecting Belgium is not that spectacular. It will have the same shape in virtually every projection: it's a small surface on an average latitude.

So let's apply some transformations on a more representative victim such as the Tropic of Cancer. This will generally be projected as a straight line, but if you stand on the North Pole -e.g. via a Gnomonic Projection- it looks like a circle:

 And with a Transverse Mercator projection it should look like an Ellipse:

 

Unfortunately the SQL Spatial Tools code only implements the Spherical version of the Transverse Mercator projection, and not (yet ?) the Ellipsoidal version. Otherwise SQL Spatial Tools would have all the ingredients for a latitude/longitude WGS84 to UTM conversion. After all, you only need to project, then scale (to convert to meters), and finally translate (for false easting). This is a horrible calculation, but don't worry: Proj.NET on CodePlex should be able to handle this (this feels like a topic for a later articleLaughing).

Anyway, here's the full solution: U2UConsult.DockOfTheBay.SpatialProjectionsSample.zip (144,07 kb).


Tuning SQL Server Lookups to a Linked Server

In SQL Server, if you join a local table with a table on a linked server (e.g. a remote Oracle instance) you should be prepared for horrible performance. In a lot of scenarios it makes a lot more sense to tell the remote server exactly what you need, store that data in a temporary table, and join locally with it. A couples of weeks ago I used this technique to bring the response time of some queries from SQL Server to a linked Oracle instance down from 500 seconds to less than one second.

Let's say we want to enumerate the countries that use the Euro as currency, like this (in AdventureWorks2008):

    SELECT c.CurrencyCode, r.Name

      FROM Sales.CountryRegionCurrency  c

INNER JOIN Person.CountryRegion r

        ON c.CountryRegionCode = r.CountryRegionCode

     WHERE c.CurrencyCode = 'EUR'

 

Here's the result:

 

Let's suppose that the names of the countries are in a separate database (e.g. an Oracle on an AS/400 box), and there's no replication in place. From SQL Server, we can get access to that source by defining a linked server. For demonstration and practical purposes -I don't have a portable AS/400 with Oracle on it- I'll create a linked server to the local SQL instance:

execute sp_addlinkedserver '.'

 

The distributed query will now look like this:

    SELECT c.CurrencyCode, r.Name

      FROM Sales.CountryRegionCurrency  c

INNER JOIN [.].AdventureWorks2008.Person.CountryRegion r

        ON c.CountryRegionCode = r.CountryRegionCode

     WHERE c.CurrencyCode = 'EUR'

 

For this particular query in this particular configuration, the response time is actually still nice (it's already five times slower, but you don't really notice that). In real-life queries -and with a real remote Oracle- you'll notice a dramatic decrease in performance. For this demo configuration, you can use Sql Profiler to reveal the query that was sent to the linked server. Instead of performing a selective look-up, SQL Server selected ALL of the rows, and forced even a SORT on it:

    SELECT "Tbl1003"."CountryRegionCode" "Col1011","Tbl1003"."Name" "Col1012"

      FROM "AdventureWorks2008"."Person"."CountryRegion" "Tbl1003"

  ORDER BY "Col1011" ASC

 

Here's a small part of the result for the query:

 

You can imagine what will happen if your lookup target is not a small local table but a large complex view. This is bad for the remote machine, the local machine and the network between the two. All of this happens because SQL Server will try to optimize its own workload, and considers the linked server as a black box (which -in the case of an AS/400- it actually ìs Wink).

What we should send to the linked server is a request for a -limited- number of key-value pairs, such as SELECT id, name FROM blablabla WHERE id in ('id1', 'id2', ...). We should send this query via the OPENQUERY function, so we can use the native SQL syntax of the remote DMBS. A classic way to create a short comma-separated list in T-SQL is with a variable and the COALESCE function. If the key is not numeric, then you need to wrap each value in quotes. OPENQUERY uses OLEDB under the hood, and this doesn't like double quotes. So you have to wrap each value in two single quotes that you have to wrap in single quotes during the concatenation. Oops, you're lost ? Just look at the code:

DECLARE @Countries VARCHAR(MAX)

 

-- Create comma-separated list of lookup values

;WITH Countries AS

(

SELECT CountryRegionCode

  FROM Sales.CountryRegionCurrency

 WHERE CurrencyCode = 'EUR'

)

SELECT @Countries = COALESCE(@Countries + ',', '') + '''''' + CountryRegionCode + ''''''

  FROM Countries

After these calls, the @Countries variable holds a comma-separated list of country codes:

 

Unfortunately OPENQUERY does'nt take parameters, so we need to construct the whole query dynamically, and call it via EXECUTE. To store the result, we need to create a temporary table, because unfortunately table variables disappear from the scope with EXECUTE:

CREATE TABLE #Countries (CountryRegionCode nvarchar(3), Name nvarchar(50))

 

DECLARE @Query VARCHAR(MAX)

 

SET @Query = 'INSERT #Countries ' +

             'SELECT * FROM OPENQUERY ([.], ' +

             '''SELECT CountryRegionCode, Name ' +

               'FROM AdventureWorks2008.Person.CountryRegion ' +

               'WHERE CountryRegionCode IN (' + @Countries + ')'')'

 

EXECUTE (@Query)

 

This is the value of the @Query variable: 

 

After these calls, the #Countries table contains the remote data (at least the fraction we're interested in):

 

So we now can join locally:

    SELECT c.CurrencyCode, r.Name

      FROM Sales.CountryRegionCurrency  c

INNER JOIN #Countries r

        ON c.CountryRegionCode = r.CountryRegionCode

     WHERE c.CurrencyCode = 'EUR'

 

And while the complexity of the code dramatically increased, the response time went down equally dramatically ...

For the sake of completeness, here's the whole demo script:

/***************/

/* Preparation */

/***************/

 

/* Add linked server to local instance */

execute sp_addlinkedserver '.'

 

USE AdventureWorks2008

GO

 

/********/

/* Test */

/********/

 

 -- Local query

    SELECT c.CurrencyCode, r.Name

      FROM Sales.CountryRegionCurrency  c

INNER JOIN Person.CountryRegion r

        ON c.CountryRegionCode = r.CountryRegionCode

     WHERE c.CurrencyCode = 'EUR'

 

 -- Query through linked server

    SELECT c.CurrencyCode, r.Name

      FROM Sales.CountryRegionCurrency  c

INNER JOIN [.].AdventureWorks2008.Person.CountryRegion r

        ON c.CountryRegionCode = r.CountryRegionCode

     WHERE c.CurrencyCode = 'EUR'

 

 -- The query sent to the linked server (from Sql Profiler)  

    SELECT "Tbl1003"."CountryRegionCode" "Col1011","Tbl1003"."Name" "Col1012"

      FROM "AdventureWorks2008"."Person"."CountryRegion" "Tbl1003"

  ORDER BY "Col1011" ASC

 

/**************/

/* Workaround */

/**************/

 

DECLARE @Countries VARCHAR(MAX)

 

-- Create comma-separated list of lookup values

;WITH Countries AS

(

SELECT CountryRegionCode

  FROM Sales.CountryRegionCurrency

 WHERE CurrencyCode = 'EUR'

)

SELECT @Countries = COALESCE(@Countries + ',', '') + '''''' + CountryRegionCode + ''''''

  FROM Countries

-- OLE DB Drivers don't like double quotes, so we have to hexuplicate ;-))

 

-- Uncomment next line for testing

-- SELECT @Countries

 

-- Create temporary table to hold results from query to linked server

CREATE TABLE #Countries (CountryRegionCode nvarchar(3), Name nvarchar(50))

 

DECLARE @Query VARCHAR(MAX)

 

-- Build query to linked server

SET @Query = 'INSERT #Countries ' +

             'SELECT * FROM OPENQUERY ([.], ' +

             '''SELECT CountryRegionCode, Name ' +

               'FROM AdventureWorks2008.Person.CountryRegion ' +

               'WHERE CountryRegionCode IN (' + @Countries + ')'')'

 

-- Uncomment next line for testing

-- SELECT @Query

 

-- Execute query to linked server

EXECUTE (@Query)

 

-- Uncomment next line for testing

-- SELECT * FROM #Countries

 

-- Execute query entirely locally

    SELECT c.CurrencyCode, r.Name

      FROM Sales.CountryRegionCurrency  c

INNER JOIN #Countries r

        ON c.CountryRegionCode = r.CountryRegionCode

     WHERE c.CurrencyCode = 'EUR'

 

DROP TABLE #Countries

 

/************/

/* Teardown */

/************/

 

/* Remove linked server */

execute sp_dropserver '.'


Displaying spatial data in WPF: from SqlGeometry to PathGeometry

This article explains how to visualize spatial data (e.g. from SQL Server 2008) in Windows Presentation Foundation without using 3rd party components or proprietary formats. We'll build a little form that allows us to enter test data in the Well-Known Text format (WKT) - manually or via copy/paste, e.g. from SQL Management Studio. A Draw button will convert our input to WPF graphics, and display it. Here's how the application looks like:

It's probably a waste of time to do the validation of the input text and the parsing of its structure ourselves, since the native spatial SQL Server .NET UDTs -SqlGeometry and SqlGeography- are specialized in that. These types are stored in the Microsoft.SqlServer.Types assembly, so we should make a reference to that one in our project. On the user interface side, the best candidate type to visualize spatial data in WPF is without any doubt the Geometry class, which represents a composite 2D-shape. To create a WPF version of spatial data, we read the WKT format and use it to initialize a SqlGeometry instance. Then we call some of the OGC-functions to break the SqlGeometry Object Model down into a PathGeometry Structure. For ease of use, it makes sense to wrap this functionality in extension methods for SqlGeometry. Here's the class:

namespace U2UConsult.DockOfTheBay

{

    using System.Windows;

    using System.Windows.Media;

    using System.Windows.Shapes;

    using Microsoft.SqlServer.Types;

 

    /// <summary>

    /// Extension Methods for SqlGeometry.

    /// </summary>

    public static class SqlGeometryExtensions

    {

        /// <summary>

        /// Translates a SqlGeometry into a Systems.Windows.Media.Geometry.

        /// </summary>

        public static Geometry AsWpfGeometry(this SqlGeometry sqlGeometry)

        {

            PathGeometry result = new PathGeometry();

 

            switch (sqlGeometry.STGeometryType().Value.ToLower())

            {

                case "point":

 

                    // Return a little 'X'

                    // (well: 'little' depends on the coordinate system ...)

                    PathFigure pointFigure = new PathFigure();

                    pointFigure.StartPoint = new Point(

                        sqlGeometry.STX.Value - .1,

                        sqlGeometry.STY.Value - .1);

                    LineSegment line = new LineSegment(

                        new Point(

                            sqlGeometry.STX.Value + .1,

                            sqlGeometry.STY.Value + .1),

                        true);

                    pointFigure.Segments.Add(line);

                    result.Figures.Add(pointFigure);

 

                    pointFigure = new PathFigure();

                    pointFigure.StartPoint = new Point(

                        sqlGeometry.STX.Value - .1,

                        sqlGeometry.STY.Value + .1);

                    line = new LineSegment(

                        new Point(

                            sqlGeometry.STX.Value + .1,

                            sqlGeometry.STY.Value - .1),

                        true);

                    pointFigure.Segments.Add(line);

                    result.Figures.Add(pointFigure);

 

                    return result;

 

                case "polygon":

 

                    // A Spacial Polygon is a collection of Rings

                    // A Ring is a Closed LineString

                    // So, return a PathFigure for each Ring

 

                    // Outer Ring

                    result.Figures.Add(LineStringToWpfGeometry(sqlGeometry.STExteriorRing()));

 

                    // Inner Rings

                    for (int i = 1; i <= sqlGeometry.STNumInteriorRing(); i++)

                    {

                        result.Figures.Add(LineStringToWpfGeometry(sqlGeometry.STInteriorRingN(i)));

                    }

 

                    return result;

 

                case "linestring":

 

                    // Return a PathFigure

                    result.Figures.Add(LineStringToWpfGeometry(sqlGeometry));

                    return result;

 

                case "multipoint":

                case "multilinestring":

                case "multipolygon":

                case "geometrycollection":

 

                    // Return a Group of Geometries

                    GeometryGroup geometryGroup = new GeometryGroup();

 

                    for (int i = 1; i <= sqlGeometry.STNumGeometries().Value; i++)

                    {

                        geometryGroup.Children.Add(sqlGeometry.STGeometryN(i).AsWpfGeometry());

                    }

 

                    return geometryGroup;

 

                default:

 

                    // Unrecognized Type

                    // Return an empty Geometry

                    return Geometry.Empty;

            }

        }

 

        /// <summary>

        /// Translates a SqlGeometry into a Systems.Windows.Shapes.Path.

        /// </summary>

        public static Path AsPath(this SqlGeometry sqlGeometry)

        {

            Path path = new Path();

            path.Data = sqlGeometry.AsWpfGeometry();

            return path;

        }

 

        /// <summary>

        /// Translates a LineString or a single Polygon Ring to a PathFigure.

        /// </summary>

        private static PathFigure LineStringToWpfGeometry(SqlGeometry sqlGeometry)

        {

            PathFigure result = new PathFigure();

 

            result.StartPoint = new Point(

                sqlGeometry.STPointN(1).STX.Value,

                sqlGeometry.STPointN(1).STY.Value);

 

            for (int i = 1; i <= sqlGeometry.STNumPoints(); i++)

            {

                LineSegment lineSegment = new LineSegment();

                lineSegment.Point = new Point(

                    sqlGeometry.STPointN(i).STX.Value,

                    sqlGeometry.STPointN(i).STY.Value);

                result.Segments.Add(lineSegment);

            }

 

            return result;

        }

    }

}

To use these extension methods, all we need to do is create a SqlGeometry instance with some data. Then we need to ensure it's valid against the OGC standards, so that the OGC compliant sql methods behave properly. Finally we call the conversion, like this:

// Make OGC Compliant

if (!sqlGeometry.STIsValid())

{

    sqlGeometry = sqlGeometry.MakeValid();

}

 

// Transformation Samples

Path path = sqlGeometry.AsPath();

Geometry geometry = sqlGeometry.AsWpfGeometry();

We end up with a Geometry that is expressed in the original spatial coordinates: latitude/longitude or X/Y against a specific SRID. So we need to translate and scale it, to project it to windows coordinates. Since we only visualize one shape, it suffices to let it stretch automatically in its container. By the way: don't forget to draw upside-down, because the origin of a Control is the upper left corner while the origin of a map is generally at the bottom left:

// Flip Y-coordinate

// Origin of a map is usually at the bottom left

path.LayoutTransform = new ScaleTransform(1, -1);

 

// Automate Translation & Inflation

path.Stretch = Stretch.Uniform;

 

Samples

I tested the code against a high number of possible (mostly Belgian) shapes. Here are some examples:

The river Zenne at the point where it leaves Brussels, heading to the north. An example of a LineString.
The province of Antwerp. An example of a MultiPolygon.
The province of Flemish Brabant. An example of a Polygon with an inner Ring.
The arrondissement Halle-Vilvoorde. An example of a Polygon.

 

Here's the full code for the WPF Window. To bring you up-to-speed immediately, it starts with a reduced shape (24 Points) of Belgium:

XAML:

<Window

   x:Class="U2UConsult.DockOfTheBay.SpatialSample"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   Title="SQL Spatial to WPF Sample"

   Icon="/U2UConsult.DockOfTheBay;component/dotbay.png">

    <Grid>

        <Grid.RowDefinitions>

            <RowDefinition Height="*" />

        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>

            <ColumnDefinition Width="*" />

            <ColumnDefinition Width="2*" />

        </Grid.ColumnDefinitions>

        <Button

           x:Name="DrawButton"

           Click="DrawButton_Click"

           Content="Draw"

           Grid.Row="0" Grid.Column="1"

           Margin="15"

           Height="32" Width="64"

           HorizontalAlignment="Left"

           VerticalAlignment="Bottom"

           />

        <!-- Reduced shape of Belgium -->

        <TextBox

           x:Name="GeometryTextBox"

           Margin="5"

           Grid.Row="0" Grid.Column="0"

           TextWrapping="Wrap"

           AcceptsReturn="True"

           AcceptsTab="True"

           Text="POLYGON ((

                    5.4695768356323242 49.499450206756592,

                    5.8744573593139648 49.576767921447754,

                    5.7810144424438477 49.959678173065186,

                    6.4083404541015625 50.333068847656250,

                    6.1721935272216800 50.550515174865723,

                    6.2783794403076172 50.616397857666016,

                    5.6911020278930664 50.761138916015625,

                    5.8341245651245117 51.168460845947266,

                    5.2405147552490234 51.261853218078613,

                    5.0372371673583984 51.485389232635500,

                    4.4786109924316406 51.480998992919922,

                    3.9020967483520508 51.198946952819824,

                    3.1825008392333984 51.361250877380371,

                    2.5581483840942383 51.093193054199219,

                    2.6278944015502930 50.814075946807861,

                    3.1747541427612305 50.752677917480469,

                    3.2816371917724609 50.526985168457031,

                    3.6048288345336914 50.489061832427979,

                    3.7020025253295900 50.300303936004639,

                    4.2115697860717773 50.269905090332031,

                    4.1991643905639648 49.960120201110840,

                    4.6723327636718750 49.985515117645264,

                    4.8746204376220700 50.151000022888184,

                    4.8553209304809570 49.794033050537109,

                    5.4695768356323242 49.499450206756592))"

           ToolTip="Place your Well-Known Text here ..."

           />

        <Border

           x:Name="DrawingCanvas"

           Padding="15"

           Grid.Row="0" Grid.Column="1"

           />

    </Grid>

</Window>

C#:

namespace U2UConsult.DockOfTheBay

{

    using System;

    using System.Windows;

    using System.Windows.Controls;

    using System.Windows.Media;

    using System.Windows.Shapes;

    using Microsoft.SqlServer.Types; // Add Reference !!!

 

    /// <summary>

    /// Demonstrates displaying SQL Server Spatial data in WPF.

    /// </summary>

    public partial class SpatialSample : Window

    {

        public SpatialSample()

        {

            InitializeComponent();

        }

 

        private void DrawButton_Click(object sender, RoutedEventArgs e)

        {

            try

            {

                // Read Well-Known Text

                SqlGeometry sqlGeometry = SqlGeometry.Parse(this.GeometryTextBox.Text);

 

                // Make OGC Compliant

                if (!sqlGeometry.STIsValid())

                {

                    sqlGeometry = sqlGeometry.MakeValid();

                }

 

                // Transform to Path

                Path path = sqlGeometry.AsPath();

 

                // Basic Properties

                path.Stroke = Brushes.Black;

                path.StrokeThickness = 1;

 

                // Polygons only ...

                //path.Effect = new DropShadowEffect() { Direction = 225 };

                //path.Fill = Brushes.DarkGreen;

 

                // Flip Y-coordinate

                // Origin of a map is usually at the bottom left

                path.LayoutTransform = new ScaleTransform(1, -1);

 

                // Automate Translation & Inflation

                path.Stretch = Stretch.Uniform;

 

                this.DrawingCanvas.Child = path;

            }

            catch (Exception ex)

            {

                // Input not valid

                this.DrawingCanvas.Child = new TextBlock()

                {

                    TextWrapping = TextWrapping.Wrap,

                    MaxHeight = 128,

                    VerticalAlignment = VerticalAlignment.Top,

                    Foreground = Brushes.Red,

                    Text = ex.Message

                };

            }

        }

    }

}


An Office WPF Ribbon Control Walkthrough

The Microsoft WPF Ribbon control is a free control that brings the Office 2007 Ribbon features to your WPF applications. To get your hands on it, just follow the instructions on CodePlex. This article walks through the Ribbon features. I simply built "NotePad with a Ribbon". Here's how it looks like:

The container is of the type RibbonWindow, a subclass of Window that overrides some of the native GDI-code to let the ApplicationButton and the QuickAccessToolBar appear in the form's title bar. The only controls on it are a Ribbon control and a TextBox (after all, Notepad ìs just a textbox with a menu). I wrapped them in a DockPanel:

<r:RibbonWindow

       x:Class="RibbonSample.RibbonSampleWindow"

       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

       xmlns:r="clr-namespace:Microsoft.Windows.Controls.Ribbon;assembly=RibbonControlsLibrary"

       Title="WPF Office Ribbon Sample">

    <DockPanel>

        <r:Ribbon

           DockPanel.Dock="Top"

           Title="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type Window}},Path=Title}">

        </r:Ribbon>

        <TextBox

           x:Name="NotePadTextBox"

           DockPanel.Dock="Top"

           AcceptsReturn="True"

           AcceptsTab="True"

           TextWrapping="Wrap" />

    </DockPanel>

</r:RibbonWindow>

The meat of the Ribbon is a series of RibbonTabs with a Label and an array of RibbonGroup elements that contain the RibbonControls (RibbonButton, RibbonComboBox), individually or grouped into a RibbonControlGroup:

<r:Ribbon

   DockPanel.Dock="Top"

   Title="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type Window}},Path=Title}">

    <r:RibbonTab Label="Home">

        <r:RibbonTab.Groups>

            <!-- Clipboard Commands -->

            <r:RibbonGroup>

                <r:RibbonGroup.Command>

                    <r:RibbonCommand LabelTitle="Clipboard" />

                </r:RibbonGroup.Command>

                <r:RibbonButton Content="Paste" />

                <r:RibbonButton Content="Copy" />

                <r:RibbonButton Content="Cut" />

            </r:RibbonGroup>

            <!-- Font Commands -->

            <r:RibbonGroup>

                <r:RibbonGroup.Command>

                    <r:RibbonCommand LabelTitle="Font" />

                </r:RibbonGroup.Command>

                <r:RibbonControlGroup Tag="FontFamily" />

                <r:RibbonControlGroup Tag="FontColor" />

            </r:RibbonGroup>

            <!-- Zoom Commands -->

            <r:RibbonGroup>

                <r:RibbonGroup.Command>

                    <r:RibbonCommand LabelTitle="Zoom" />

                </r:RibbonGroup.Command>

                <r:RibbonButton Content="Zoom In" />

                <r:RibbonButton Content="Zoom Out" />

            </r:RibbonGroup>

        </r:RibbonTab.Groups>

    </r:RibbonTab>

    <r:RibbonTab Label="View" />

</r:Ribbon>

RibbonGroup has a collection of GroupSizeDefinitions that control layout and resizing. This is the configuration of my Clipboard-group, with one large and two small icons. You can put multiple alternatives here; when you resize the form, the Ribbon will layout properly. 

<r:RibbonGroup.GroupSizeDefinitions>

    <r:RibbonGroupSizeDefinitionCollection>

        <r:RibbonGroupSizeDefinition>

            <r:RibbonControlSizeDefinition

               ImageSize="Large"

               IsLabelVisible="True"/>

            <r:RibbonControlSizeDefinition

               ImageSize="Small"

               IsLabelVisible="True"/>

            <r:RibbonControlSizeDefinition

               ImageSize="Small"

               IsLabelVisible="True"/>

        </r:RibbonGroupSizeDefinition>

    </r:RibbonGroupSizeDefinitionCollection>

</r:RibbonGroup.GroupSizeDefinitions>

All RibbonControls can have a Command property, and instance of RibbonCommand. This is actually a subclass of RoutedCommand, implementing extra GUI-focused properties: labels, tooltips, and large and small image sources.

<r:RibbonButton Content="Paste">

    <r:RibbonButton.Command>

        <r:RibbonCommand

           CanExecute="PasteCommand_CanExecute"

           Executed="PasteCommand_Executed"

           LabelTitle="Paste"

           ToolTipTitle="Paste"

           ToolTipDescription="Paste text element from the clipboard."

           SmallImageSource="Assets/Images/Paste.png"

           LargeImageSource="Assets/Images/Paste.png" />

    </r:RibbonButton.Command>

</r:RibbonButton>

Controls can be (re-)grouped into a RibbonControlGroup e.g. to form a Gallery. I used two such groups for the font commands:

 

<!-- Font Commands -->

<r:RibbonGroup>

    <r:RibbonGroup.Command>

        <r:RibbonCommand LabelTitle="Font" />

    </r:RibbonGroup.Command>

    <r:RibbonControlGroup>

        <r:RibbonLabel Tag="Font Image" />

        <r:RibbonComboBox>

            <r:RibbonComboBoxItem Content="Tahoma"/>

            <r:RibbonComboBoxItem Content="Comic Sans MS"/>

            <r:RibbonComboBoxItem Content="Wingdings"/>

        </r:RibbonComboBox>

    </r:RibbonControlGroup>

    <r:RibbonControlGroup>

        <r:RibbonLabel Tag="Pallette Image" />

        <r:RibbonButton CommandParameter="Black" />

        <r:RibbonButton CommandParameter="Red" />

        <r:RibbonButton CommandParameter="Blue" />

        <r:RibbonButton CommandParameter="Green" />

    </r:RibbonControlGroup>

</r:RibbonGroup>

Clicking on the Application Button (the large circle in the top left corner) reveals the Ribbon's ApplicationMenu. This can be populated with RibbonApplicationMenuItems, Separators, and RibbonApplicationMenuSplitItems. It also hosts the RecentItemsList. In the ApplicationMenu's footer there's room for extra buttons.

ApplicationMenu
RecentItemsList
ApplicationSplitMenu

 

<!-- The Application Button -->

<r:Ribbon.ApplicationMenu>

    <r:RibbonApplicationMenu>

        <r:RibbonApplicationMenu.RecentItemList>

            <r:RibbonHighlightingList

               MostRecentFileSelected="RibbonHighlightingList_MostRecentFileSelected">

                <r:RibbonHighlightingListItem Content="..." />

            </r:RibbonHighlightingList>

        </r:RibbonApplicationMenu.RecentItemList>

        <r:RibbonApplicationMenuItem>

            <r:RibbonApplicationMenuItem.Command>

                <r:RibbonCommand Executed="FileNew_Executed" />

            </r:RibbonApplicationMenuItem.Command>

        </r:RibbonApplicationMenuItem>

        <Separator />

        <r:RibbonApplicationSplitMenuItem>

            <r:RibbonApplicationMenuItem>

                <r:RibbonApplicationMenuItem.Command>

                    <r:RibbonCommand Executed="Send_As_SMS" />

                </r:RibbonApplicationMenuItem.Command>

            </r:RibbonApplicationMenuItem>

            <r:RibbonApplicationMenuItem>

                <r:RibbonApplicationMenuItem.Command>

                    <r:RibbonCommand Executed="Send_As_Email" />

                </r:RibbonApplicationMenuItem.Command>

            </r:RibbonApplicationMenuItem>

        </r:RibbonApplicationSplitMenuItem>

        <r:RibbonApplicationMenu.Footer>

            <DockPanel LastChildFill="False">

                <r:RibbonButton DockPanel.Dock="Right" Tag="Close" />

                <r:RibbonButton DockPanel.Dock="Right" Tag="Options" />

            </DockPanel>

        </r:RibbonApplicationMenu.Footer>

    </r:RibbonApplicationMenu>

</r:Ribbon.ApplicationMenu>

Yet another host for commands is the QuickAccessToolBar, which can be placed in the form's header next to the ApplicationButton, or under the Ribbon:

<!-- Quick Access ToolBar -->

<r:Ribbon.QuickAccessToolBar>

    <r:RibbonQuickAccessToolBar>

        <r:RibbonButton

           r:RibbonQuickAccessToolBar.Placement="InCustomizeMenuAndToolBar">

            <r:RibbonButton.Command>

                <r:RibbonCommand

                       Executed="EncryptCommand_Executed" />

            </r:RibbonButton.Command>

        </r:RibbonButton>

        <r:RibbonButton

           r:RibbonQuickAccessToolBar.Placement="InCustomizeMenuAndToolBar">

            <r:RibbonButton.Command>

                <r:RibbonCommand

                       Executed="DecryptCommand_Executed" />

            </r:RibbonButton.Command>

        </r:RibbonButton>

    </r:RibbonQuickAccessToolBar>

</r:Ribbon.QuickAccessToolBar>

If you're going to use Microsoft's WPF Ribbon Control in production, please note that the current release is just a preview, and V1 will have breaking changes in it. On the other hand, a release date hasn't been announced for this V1, all we know is that the control will not be included in .NET 4.0 / Visual Studio.NET 2010.

Here's the full code for the project. Unfortunately I may not publish the full solution, due to licensing constraints on the Ribbon Control. You will need to accept the license, download the ribbon dll yourself, and add a reference.

SOLUTION STRUCTURE

XAML

<r:RibbonWindow x:Class="RibbonSample.RibbonSampleWindow"

       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

       xmlns:r="clr-namespace:Microsoft.Windows.Controls.Ribbon;assembly=RibbonControlsLibrary"

       xmlns:local="clr-namespace:RibbonSample"

       Title="WPF Office Ribbon Sample"

       Icon="/RibbonSample;component/Assets/Images/dotbay.png">

    <Window.Resources>

        <ResourceDictionary>

            <ResourceDictionary.MergedDictionaries>

                <ResourceDictionary Source="/RibbonControlsLibrary;component/Themes/Office2007Blue.xaml" />

            </ResourceDictionary.MergedDictionaries>

        </ResourceDictionary>

    </Window.Resources>

    <DockPanel>

        <r:Ribbon

           DockPanel.Dock="Top"

           Title="{Binding RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type Window}},Path=Title}"

           >

            <!-- The Application Button -->

            <r:Ribbon.ApplicationMenu>

                <r:RibbonApplicationMenu>

                    <r:RibbonApplicationMenu.Command>

                        <r:RibbonCommand

                           LabelTitle="Application Button"

                           LabelDescription="Close the application."

                           ToolTipTitle="WPF Office Ribbon Sample"

                           ToolTipDescription="Überstyled notepad."

                           SmallImageSource="/RibbonSample;component/Assets/Images/dotbay.png"

                           LargeImageSource="/RibbonSample;component/Assets/Images/dotbay.png"/>

                    </r:RibbonApplicationMenu.Command>

                    <r:RibbonApplicationMenu.RecentItemList>

                        <r:RibbonHighlightingList MostRecentFileSelected="RibbonHighlightingList_MostRecentFileSelected">

                            <r:RibbonHighlightingListItem Content="random1.txt" />

                            <r:RibbonHighlightingListItem Content="random2.txt" />

                            <r:RibbonHighlightingListItem Content="random3.txt" />

                            <r:RibbonHighlightingListItem Content="random4.txt" />

                        </r:RibbonHighlightingList>

                    </r:RibbonApplicationMenu.RecentItemList>

                    <r:RibbonApplicationMenuItem>

                        <r:RibbonApplicationMenuItem.Command>

                            <r:RibbonCommand

                               Executed="FileNew_Executed"

                               LabelTitle="New"

                               ToolTipTitle="New"

                               ToolTipDescription="Create a new File"

                               LargeImageSource="/RibbonSample;component/Assets/Images/FileNew.png"

                               SmallImageSource="/RibbonSample;component/Assets/Images/FileNew.png" />

                        </r:RibbonApplicationMenuItem.Command>

                    </r:RibbonApplicationMenuItem>

                    <r:RibbonApplicationMenuItem>

                        <r:RibbonApplicationMenuItem.Command>

                            <r:RibbonCommand

                               Executed="FileOpen_Executed"

                               LabelTitle="Open"

                               ToolTipTitle="Open"

                               ToolTipDescription="Open an existing File"

                               LargeImageSource="/RibbonSample;component/Assets/Images/FileOpen.png"

                               SmallImageSource="/RibbonSample;component/Assets/Images/FileOpen.png" />

                        </r:RibbonApplicationMenuItem.Command>

                    </r:RibbonApplicationMenuItem>

                    <r:RibbonApplicationMenuItem>

                        <r:RibbonApplicationMenuItem.Command>

                            <r:RibbonCommand

                               CanExecute="CanAlwaysExecute"

                               Executed="FileSave_Executed"

                               LabelTitle="Save"

                               ToolTipTitle="Save"

                               ToolTipDescription="Save the current File"

                               LargeImageSource="/RibbonSample;component/Assets/Images/FileSave.png"

                               SmallImageSource="/RibbonSample;component/Assets/Images/FileSave.png" />

                        </r:RibbonApplicationMenuItem.Command>

                    </r:RibbonApplicationMenuItem>

                    <Separator />

                    <r:RibbonApplicationSplitMenuItem>

                        <r:RibbonApplicationSplitMenuItem.Command>

                            <r:RibbonCommand

                                   Executed="Not_Yet_Implemented"

                                   LabelTitle="Publish"

                                   LabelDescription="Expose this text to the world"

                                   ToolTipTitle="Publish"

                                   ToolTipDescription="Choose your technology."

                                   SmallImageSource="Assets/Images/publish.png"

                                   LargeImageSource="Assets/Images/publish.png" />

                        </r:RibbonApplicationSplitMenuItem.Command>

                        <r:RibbonApplicationMenuItem>

                            <r:RibbonApplicationMenuItem.Command>

                                <r:RibbonCommand

                                   Executed="Not_Yet_Implemented"

                                   LabelTitle="Send as SMS"

                                   ToolTipTitle="Send as SMS"

                                   ToolTipDescription="Send as an SMS."

                                   SmallImageSource="Assets/Images/send_sms.png"

                                   LargeImageSource="Assets/Images/send_sms.png" />

                            </r:RibbonApplicationMenuItem.Command>

                        </r:RibbonApplicationMenuItem>

                        <r:RibbonApplicationMenuItem>

                            <r:RibbonApplicationMenuItem.Command>

                                <r:RibbonCommand

                                   Executed="Not_Yet_Implemented"

                                   LabelTitle="Send as e-mail"

                                   ToolTipTitle="Send as mail"

                                   ToolTipDescription="Send as an e-mail."

                                   SmallImageSource="Assets/Images/send_mail.png"

                                   LargeImageSource="Assets/Images/send_mail.png" />

                            </r:RibbonApplicationMenuItem.Command>

                        </r:RibbonApplicationMenuItem>

                        <r:RibbonApplicationMenuItem>

                            <r:RibbonApplicationMenuItem.Command>

                                <r:RibbonCommand

                                   Executed="Not_Yet_Implemented"

                                   LabelTitle="Send as tweet"

                                   LabelDescription="twit twit twit"

                                   ToolTipTitle="Send as tweet"

                                   ToolTipDescription="Send via Twitter."

                                   SmallImageSource="Assets/Images/send_tweet.png"

                                   LargeImageSource="Assets/Images/send_tweet.png" />

                            </r:RibbonApplicationMenuItem.Command>

                        </r:RibbonApplicationMenuItem>

                    </r:RibbonApplicationSplitMenuItem>

                    <r:RibbonApplicationMenu.Footer>

                        <DockPanel LastChildFill="False">

                            <r:RibbonButton

                               Margin="2"

                               DockPanel.Dock="Right"

                               >

                                <r:RibbonButton.Command>

                                    <r:RibbonCommand

                                   Executed="Not_Yet_Implemented"

                                   LabelTitle="Close"

                                   ToolTipTitle="Close"

                                   ToolTipDescription="Close the Application Menu."

                                   SmallImageSource="Assets/Images/Exit.png"

                                   LargeImageSource="Assets/Images/Exit.png" />

                                </r:RibbonButton.Command>

                            </r:RibbonButton>

                            <r:RibbonButton

                               DockPanel.Dock="Right"

                               >

                                <r:RibbonButton.Command>

                                    <r:RibbonCommand

                                   Executed="Not_Yet_Implemented"

                                   LabelTitle="Options"

                                   ToolTipTitle="Options"

                                   ToolTipDescription="Extra options."

                                   SmallImageSource="Assets/Images/Configuration.png"

                                   LargeImageSource="Assets/Images/Configuration.png" />

                                </r:RibbonButton.Command>

                            </r:RibbonButton>

                        </DockPanel>

                    </r:RibbonApplicationMenu.Footer>

                </r:RibbonApplicationMenu>

            </r:Ribbon.ApplicationMenu>

            <!-- Quick Access ToolBar -->

            <r:Ribbon.QuickAccessToolBar>

                <r:RibbonQuickAccessToolBar>

                    <r:RibbonButton

                       r:RibbonQuickAccessToolBar.Placement="InCustomizeMenuAndToolBar">

                        <r:RibbonButton.Command>

                            <r:RibbonCommand

                                   CanExecute="EncryptCommand_CanExecute"

                                   Executed="EncryptCommand_Executed"

                                   LabelTitle="Encrypt"

                                   ToolTipTitle="Encrypt"

                                   ToolTipDescription="Clear all text."

                                   SmallImageSource="Assets/Images/Encrypt.png"

                                   LargeImageSource="Assets/Images/Encrypt.png" />

                        </r:RibbonButton.Command>

                    </r:RibbonButton>

                    <r:RibbonButton

                       r:RibbonQuickAccessToolBar.Placement="InCustomizeMenuAndToolBar">

                        <r:RibbonButton.Command>

                            <r:RibbonCommand

                                   CanExecute="DecryptCommand_CanExecute"

                                   Executed="DecryptCommand_Executed"

                                   LabelTitle="Decrypt"

                                   ToolTipTitle="Decrypt"

                                   ToolTipDescription="Uncipher the text."

                                   SmallImageSource="Assets/Images/Decrypt.png"

                                   LargeImageSource="Assets/Images/Decrypt.png" />

                        </r:RibbonButton.Command>

                    </r:RibbonButton>

                </r:RibbonQuickAccessToolBar>

            </r:Ribbon.QuickAccessToolBar>

            <r:RibbonTab Label="Home">

                <r:RibbonTab.Groups>

                    <!-- Clipboard Commands -->

                    <r:RibbonGroup>

                        <r:RibbonGroup.GroupSizeDefinitions>

                            <r:RibbonGroupSizeDefinitionCollection>

                                <r:RibbonGroupSizeDefinition>

                                    <r:RibbonControlSizeDefinition

                                       ImageSize="Large"

                                       IsLabelVisible="True"/>

                                    <r:RibbonControlSizeDefinition

                                       ImageSize="Small"

                                       IsLabelVisible="True"/>

                                    <r:RibbonControlSizeDefinition

                                       ImageSize="Small"

                                       IsLabelVisible="True"/>

                                </r:RibbonGroupSizeDefinition>

                            </r:RibbonGroupSizeDefinitionCollection>

                        </r:RibbonGroup.GroupSizeDefinitions>

                        <r:RibbonGroup.Command>

                            <r:RibbonCommand LabelTitle="Clipboard" />

                        </r:RibbonGroup.Command>

                        <r:RibbonButton Content="Paste">

                            <r:RibbonButton.Command>

                                <r:RibbonCommand

                                   CanExecute="PasteCommand_CanExecute"

                                   Executed="PasteCommand_Executed"

                                   LabelTitle="Paste"

                                   ToolTipTitle="Paste"

                                   ToolTipDescription="Paste text element from the clipboard."

                                   SmallImageSource="Assets/Images/Paste.png"

                                   LargeImageSource="Assets/Images/Paste.png" />

                            </r:RibbonButton.Command>

                        </r:RibbonButton>

                        <r:RibbonButton Content="Copy">

                            <r:RibbonButton.Command>

                                <r:RibbonCommand

                                   CanExecute="CopyCommand_CanExecute"

                                   Executed="CopyCommand_Executed"

                                   LabelTitle="Copy"

                                   ToolTipTitle="Copy"

                                   ToolTipDescription="Copy text element to the clipboard."

                                   SmallImageSource="Assets/Images/Copy.png"

                                   LargeImageSource="Assets/Images/Copy.png" />

                            </r:RibbonButton.Command>

                        </r:RibbonButton>

                        <r:RibbonButton Content="Cut">

                            <r:RibbonButton.Command>

                                <r:RibbonCommand

                                   CanExecute="CutCommand_CanExecute"

                                   Executed="CutCommand_Executed"

                                   LabelTitle="Cut"

                                   ToolTipTitle="Cut"

                                   ToolTipDescription="Cut text element to the clipboard."

                                   SmallImageSource="Assets/Images/Cut.png"

                                   LargeImageSource="Assets/Images/Cut.png" />

                            </r:RibbonButton.Command>

                        </r:RibbonButton>

                    </r:RibbonGroup>

                    <!-- Font Commands -->

                    <r:RibbonGroup>

                        <r:RibbonGroup.GroupSizeDefinitions>

                            <r:RibbonGroupSizeDefinitionCollection>

                                <r:RibbonGroupSizeDefinition>

                                    <r:RibbonControlSizeDefinition

                                       ImageSize="Small"

                                       IsLabelVisible="True"/>

                                    <r:RibbonControlSizeDefinition

                                       ImageSize="Small"

                                       IsLabelVisible="True"/>

                                </r:RibbonGroupSizeDefinition>

                            </r:RibbonGroupSizeDefinitionCollection>

                        </r:RibbonGroup.GroupSizeDefinitions>

                        <r:RibbonGroup.Command>

                            <r:RibbonCommand LabelTitle="Font" />

                        </r:RibbonGroup.Command>

                        <r:RibbonControlGroup Margin=" 0 3 0 3">

                            <r:RibbonLabel

                               Padding="0 3" Margin="3 0"

                               VerticalAlignment="Bottom">

                                <r:RibbonLabel.Content>

                                    <Image

                                       Margin="0" Height="16"

                                       Source="/RibbonSample;component/Assets/Images/Font.png" />

                                </r:RibbonLabel.Content>

                            </r:RibbonLabel>

                            <r:RibbonComboBox

                               x:Name="FontComboBox"

                               SelectionChanged="FontComboBox_SelectionChanged"

                               SelectedIndex="0"

                               MinWidth="120"

                               IsReadOnly="True"

                               IsEditable="True">

                                <r:RibbonComboBoxItem Content="Tahoma"/>

                                <r:RibbonComboBoxItem Content="Comic Sans MS"/>

                                <r:RibbonComboBoxItem Content="Wingdings"/>

                            </r:RibbonComboBox>

                        </r:RibbonControlGroup>

                        <r:RibbonControlGroup Height="28">

                            <r:RibbonLabel

                               Padding="0 6" Margin="3 0" VerticalAlignment="Bottom">

                                <r:RibbonLabel.Content>

                                    <Image

                                       Margin="0" Height="16"

                                       Source="/RibbonSample;component/Assets/Images/Palette.png" />

                                </r:RibbonLabel.Content>

                            </r:RibbonLabel>

                            <r:RibbonButton

                               CommandParameter="Black"

                               ToolTip="Black"

                               Background="Black"

                               Margin="0 3 4 3">

                                <r:RibbonButton.Command>

                                    <r:RibbonCommand

                                       Executed="FontColorCommand_Executed"/>

                                </r:RibbonButton.Command>

                            </r:RibbonButton>

                            <r:RibbonButton

                               CommandParameter="Red"

                               ToolTip="Red"

                               Background="Red"

                               Margin="4 3">

                                <r:RibbonButton.Command>

                                    <r:RibbonCommand

                                       Executed="FontColorCommand_Executed"/>

                                </r:RibbonButton.Command>

                            </r:RibbonButton>

                            <r:RibbonButton

                               CommandParameter="Green"

                               ToolTip="Green"

                               Background="Green"

                               Margin="4 3">

                                <r:RibbonButton.Command>

                                    <r:RibbonCommand

                                       Executed="FontColorCommand_Executed"/>

                                </r:RibbonButton.Command>

                            </r:RibbonButton>

                            <r:RibbonButton

                               CommandParameter="Blue"

                               ToolTip="Blue"

                               Background="Blue"

                               Margin="4 3">

                                <r:RibbonButton.Command>

                                    <r:RibbonCommand

                                       Executed="FontColorCommand_Executed"/>

                                </r:RibbonButton.Command>

                            </r:RibbonButton>

                        </r:RibbonControlGroup>

                    </r:RibbonGroup>

                    <!-- Zoom Commands -->

                    <r:RibbonGroup>

                        <r:RibbonGroup.GroupSizeDefinitions>

                            <r:RibbonGroupSizeDefinitionCollection>

                                <r:RibbonGroupSizeDefinition>

                                    <r:RibbonControlSizeDefinition

                                       ImageSize="Large"

                                       IsLabelVisible="True"/>

                                    <r:RibbonControlSizeDefinition

                                       ImageSize="Large"

                                       IsLabelVisible="True"/>

                                </r:RibbonGroupSizeDefinition>

                            </r:RibbonGroupSizeDefinitionCollection>

                        </r:RibbonGroup.GroupSizeDefinitions>

                        <r:RibbonGroup.Command>

                            <r:RibbonCommand LabelTitle="Zoom" />

                        </r:RibbonGroup.Command>

                        <r:RibbonButton Content="Zoom In">

                            <r:RibbonButton.Command>

                                <r:RibbonCommand

                                   Executed="ZoomInCommand_Executed"

                                   LabelTitle="In"

                                   ToolTipTitle="Zoom In"

                                   ToolTipDescription="Zoom In."

                                   SmallImageSource="Assets/Images/Zoom-in.png"

                                   LargeImageSource="Assets/Images/Zoom-in.png" />

                            </r:RibbonButton.Command>

                        </r:RibbonButton>

                        <r:RibbonButton Content="Zoom Out">

                            <r:RibbonButton.Command>

                                <r:RibbonCommand

                                   CanExecute="ZoomOutCommand_CanExecute"

                                   Executed="ZoomOutCommand_Executed"

                                   LabelTitle="Out"

                                   ToolTipTitle="Zoom Out"

                                   ToolTipDescription="Zoom Out."

                                   SmallImageSource="Assets/Images/Zoom-out.png"

                                   LargeImageSource="Assets/Images/Zoom-out.png" />

                            </r:RibbonButton.Command>

                        </r:RibbonButton>

                    </r:RibbonGroup>

                </r:RibbonTab.Groups>

            </r:RibbonTab>

            <r:RibbonTab Label="View">

            </r:RibbonTab>

        </r:Ribbon>

        <TextBox

           x:Name="NotePadTextBox"

           DockPanel.Dock="Top"

           AcceptsReturn="True"

           AcceptsTab="True"

           TextWrapping="Wrap"

           />

    </DockPanel>

</r:RibbonWindow>

C#

namespace RibbonSample

{

    using System;

    using System.IO;

    using System.Text;

    using System.Windows.Input;

    using System.Windows.Media;

    using Microsoft.Windows.Controls.Ribbon;

    using Microsoft.Win32;

 

    public partial class RibbonSampleWindow : RibbonWindow

    {

        public RibbonSampleWindow()

        {

            InitializeComponent();

 

            this.IsEncrypted = false;

 

            // Programmatically setting the skin:

            // this.Resources.MergedDictionaries.Add(PopularApplicationSkins.Office2007Blue);

 

            this.NotePadTextBox.Focus();

        }

 

        public bool IsEncrypted { get; set; }

 

        public string FileName { get; set; }

 

        #region Quick Access Toolbar Commands

        private void EncryptCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)

        {

            e.CanExecute = !this.IsEncrypted;

        }

 

        private void EncryptCommand_Executed(object sender, ExecutedRoutedEventArgs e)

        {

            // No real Rijndael, just obfuscation !!!

            this.NotePadTextBox.Text =

                Convert.ToBase64String(ASCIIEncoding.ASCII.GetBytes(this.NotePadTextBox.Text));

            this.NotePadTextBox.IsEnabled = false;

            this.IsEncrypted = true;

        }

 

        private void DecryptCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)

        {

            e.CanExecute = this.IsEncrypted;

        }

 

        private void DecryptCommand_Executed(object sender, ExecutedRoutedEventArgs e)

        {

            this.NotePadTextBox.Text =

                ASCIIEncoding.ASCII.GetString(Convert.FromBase64String(this.NotePadTextBox.Text));

            this.NotePadTextBox.IsEnabled = true;

            this.IsEncrypted = false;

        }

 

        #endregion

 

        #region Application Button Commands

 

        private void RibbonHighlightingList_MostRecentFileSelected(object sender, MostRecentFileSelectedEventArgs e)

        {

            this.FileName = (string)e.SelectedItem;

            this.Title = this.FileName;

        }

 

        private void FileNew_Executed(object sender, ExecutedRoutedEventArgs e)

        {

            this.NotePadTextBox.Text = string.Empty;

            this.FileName = "New Document.txt";

            this.Title = this.FileName;

        }

 

        private void FileOpen_Executed(object sender, ExecutedRoutedEventArgs e)

        {

            OpenFileDialog dlg = new OpenFileDialog();

            dlg.FileName = "Document";

            dlg.DefaultExt = ".txt";

            dlg.Filter = "Text documents (.txt)|*.txt";

 

            if (dlg.ShowDialog() == true)

            {

                this.FileName = dlg.FileName;

                this.Title = FileName;

                this.NotePadTextBox.Text = File.ReadAllText(this.FileName);

            }

        }

 

        private void FileSave_Executed(object sender, ExecutedRoutedEventArgs e)

        {

            SaveFileDialog dlg = new SaveFileDialog();

            if (!string.IsNullOrEmpty(this.FileName))

            {

                dlg.FileName = this.FileName;

            }

            else

            {

                dlg.FileName = "Document";

            }

            dlg.DefaultExt = ".text";

            dlg.Filter = "Text documents (.txt)|*.txt";

            if (dlg.ShowDialog() == true)

            {

                FileName = dlg.FileName;

                this.Title = FileName;

                File.WriteAllText(FileName, this.NotePadTextBox.Text);

            }

        }

 

        #endregion

 

        #region ClipBoard Commands

 

        private void CopyCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)

        {

            e.CanExecute = ApplicationCommands.Copy.CanExecute(null, null);

        }

 

        private void PasteCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)

        {

            e.CanExecute = ApplicationCommands.Paste.CanExecute(null, null);

        }

 

        private void CutCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)

        {

            e.CanExecute = ApplicationCommands.Cut.CanExecute(null, null);

        }

 

        private void PasteCommand_Executed(object sender, ExecutedRoutedEventArgs e)

        {

            ApplicationCommands.Paste.Execute(null, null);

        }

 

        private void CopyCommand_Executed(object sender, ExecutedRoutedEventArgs e)

        {

            ApplicationCommands.Copy.Execute(null, null);

        }

 

        private void CutCommand_Executed(object sender, ExecutedRoutedEventArgs e)

        {

            ApplicationCommands.Cut.Execute(null, null);

        }

 

        #endregion

 

        #region Font Commands

 

        private void FontComboBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)

        {

            if (this.NotePadTextBox != null)

            {

                switch (this.FontComboBox.SelectedIndex)

                {

                    case 0:

                        this.NotePadTextBox.FontFamily = new FontFamily("Tahoma");

                        return;

                    case 1:

                        this.NotePadTextBox.FontFamily = new FontFamily("Comic Sans MS");

                        return;

                    case 2:

                        this.NotePadTextBox.FontFamily = new FontFamily("Wingdings");

                        return;

                    default:

                        return;

                }

            }

        }

 

        private void FontColorCommand_Executed(object sender, ExecutedRoutedEventArgs e)

        {

            this.NotePadTextBox.Foreground =

                new SolidColorBrush(

                    (Color)ColorConverter.ConvertFromString(e.Parameter as string));

        }

 

        #endregion

 

        #region Zoom Commands

 

        private void ZoomInCommand_Executed(object sender, ExecutedRoutedEventArgs e)

        {

            this.NotePadTextBox.FontSize++;

        }

 

        private void ZoomOutCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)

        {

            e.CanExecute = (this.NotePadTextBox.FontSize > 1);

        }

 

        private void ZoomOutCommand_Executed(object sender, ExecutedRoutedEventArgs e)

        {

            this.NotePadTextBox.FontSize--;

        }

 

        #endregion

 

        #region Helper Commands

 

        private void CanAlwaysExecute(object sender, CanExecuteRoutedEventArgs e)

        {

            e.CanExecute = true;

        }

 

        private void Not_Yet_Implemented(object sender, ExecutedRoutedEventArgs e)

        {

            // Not yet implemented ...

        }

 

        #endregion

    }

}


Charting with WPF and Silverlight

Sometimes we need to decorate our WPF or Silverlight applications with things like bar charts or pie charts. There's no need to create these from scratch, since a lot of charting solutions are available, some of which are free while other are ... less free. On CodePlex you find the Silverlight Toolkit and the WPF Toolkit. The charting controls of these toolkits share the same object model -well, they actually have even the same source code. Programming against these controls is easy, but you should revisit your source code after each release. It's fast moving beta software, but fortunately it's moving in the right direction: some of the components will eventually become part of the .NET 4.x framework. In their current implementation, the charting controls behave maturely at run time, but at design time you might encounter some issues. Don't worry too much about these: it's safe to simply ignore most of the XAML warnings and errors.

The Chart class allows you to build the following chart types:

  • Bar Chart
  • Column Chart
  • Line Chart
  • Pie Chart
  • Scatter Chart
  • Bubble Chart
  • Area Chart
  • Tree Map

Examples of each chart can be found here.

The fastest way to explore the charting controls is through the Chart Builder project. It's a wizard that allows you to generate the XAML for simple charts. Here's how it looks like:

Your next step would be to download the Data Visualizations Demo solution to figure out how more sophisticated charts can be generated. From then on, regularly check Delay's Blog to keep in touch with the latest developments.

Let's get our hands dirty and build two small samples.

1. Building a Pareto Chart in XAML

A Chart Control instance is not much more than a container for data series and axes. So it's easy to build combined charts like a standard Pareto chart. A standard Pareto chart combines a column chart displaying absolute values with a line chart representing the cumulative percentage, like this one:

A chart like this will certainly impress your end user! The corresponding XAML is amazingly straightforward. It's a chart with a column series and a line series that share the same X-axis. I'm just going to just let the XAML speak for itself:

<Window x:Class="U2UConsult.DockOfTheBay.ParetoChartWindow"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   xmlns:sys="clr-namespace:System;assembly=mscorlib"

   xmlns:collections="clr-namespace:System.Collections;assembly=mscorlib"

   xmlns:charting="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit"

   Title="Data Visualization Sample"

   Icon="/DataVisualizationSample;component/dotbay.png">

    <Grid>

        <!-- Pareto Chart -->

        <charting:Chart

           Title="A Pareto Chart"

           LegendTitle="Legend" >

            <!-- Common X axis -->

            <charting:Chart.Axes>

                <charting:CategoryAxis Orientation="X" />

            </charting:Chart.Axes>

            <charting:Chart.Series>

                <!-- Column series with absolute values -->

                <charting:ColumnSeries Title="Absolute Values">

                    <!-- Y axis on the left side -->

                    <charting:ColumnSeries.DependentRangeAxis>

                        <charting:LinearAxis

                           Orientation="Y"

                           Minimum="0"

                           Maximum="50"

                           Location="Left" />

                    </charting:ColumnSeries.DependentRangeAxis>

                    <!-- Data -->

                    <charting:ColumnSeries.ItemsSource>

                        <collections:ArrayList>

                            <sys:Double>23</sys:Double>

                            <sys:Double>15</sys:Double>

                            <sys:Double>11</sys:Double>

                            <sys:Double>10</sys:Double>

                            <sys:Double>7</sys:Double>

                            <sys:Double>5</sys:Double>

                            <sys:Double>4</sys:Double>

                        </collections:ArrayList>

                    </charting:ColumnSeries.ItemsSource>

                </charting:ColumnSeries>

                <!-- Line series with cumulative percentage -->

                <charting:LineSeries

                   Title="Cumulative Percentage"

                   IndependentValueBinding="{Binding}" >

                    <!-- Y axis on the right side -->

                    <charting:LineSeries.DependentRangeAxis>

                        <charting:LinearAxis

                           Orientation="Y"

                           Minimum="0"

                           Maximum="100"

                           Location="Right" />

                    </charting:LineSeries.DependentRangeAxis>

                    <!-- X axis reuses the same categories -->

                    <charting:LineSeries.IndependentAxis>

                        <charting:CategoryAxis

                           Orientation="X" />

                    </charting:LineSeries.IndependentAxis>

                    <!-- Data -->

                    <charting:LineSeries.ItemsSource>

                        <collections:ArrayList>

                            <sys:Double>31</sys:Double>

                            <sys:Double>51</sys:Double>

                            <sys:Double>65</sys:Double>

                            <sys:Double>79</sys:Double>

                            <sys:Double>88</sys:Double>

                            <sys:Double>95</sys:Double>

                            <sys:Double>100</sys:Double>

                        </collections:ArrayList>

                    </charting:LineSeries.ItemsSource>

                </charting:LineSeries>

            </charting:Chart.Series>

        </charting:Chart>

    </Grid>

</Window>

 

2. Building a Bubble Chart

The bubble chart is one of my favorites, because it can easily display 4 values at a time:

  • a value on the X axis,
  • a value on the Y axis,
  • the size of the bubble, and last but not least
  • a value in the tooltip.

Here's an example:

For starters, let's build a class with four instance properties, and a static property returning a list:

namespace U2UConsult.DockOfTheBay

{

    using System.Collections.Generic;

    using System.Collections.ObjectModel;

 

    /// <summary>

    /// The starship that boldly went where nobody went before.

    /// </summary>

    public class Enterprise

    {

        /// <summary>

        /// Gets a collection of all versions of the USS Enterprise.

        /// </summary>

        public static ObservableCollection<Enterprise> GetAll

        {

            get

            {

                return new ObservableCollection<Enterprise>()

                {

                    new Enterprise() {

                        Name = "NCC-1701-A", Length = 305, CrewSize = 432, CommandingOfficer = "James T. Kirk"

                    },

                    new Enterprise() {

                        Name = "NCC-1701-B", Length = 511, CrewSize = 750, CommandingOfficer = "John Harriman"

                    },

                    new Enterprise() {

                        Name = "NCC-1701-C", Length = 526, CrewSize = 700, CommandingOfficer = "Rachel Garrett"

                    },

                    new Enterprise() {

                        Name = "NCC-1701-D", Length = 642, CrewSize = 1012, CommandingOfficer = "Jean-Luc Picard"

                    },

                    new Enterprise() {

                        Name = "NCC-1701-E", Length = 685, CrewSize = 855, CommandingOfficer = "Morgan Bateson"

                    }

                };

            }

        }

 

        /// <summary>

        /// Gets or sets the registered name.

        /// </summary>

        public string Name { get; set; }

 

        /// <summary>

        /// Gets or sets the number of crew members.

        /// </summary>

        public int CrewSize { get; set; }

 

        /// <summary>

        /// Gets or sets the name of the initial commmanding officer.

        /// </summary>

        public string CommandingOfficer { get; set; }

 

        /// <summary>

        /// Gets or sets the lenght of the ship.

        /// </summary>

        /// <remarks>Expressed in metric meters.</remarks>

        public int Length { get; set; }

    }

}

 

The Lenght, Name, and CrewSize properties are respectively bound through

  • a DependentValueBinding (Y axis),
  • a IndependentValueBinding (X axis), and
  • a SizeValueBinding (size of the bubble).

The initial version of the XAML looks like this:

<Window x:Class="U2UConsult.DockOfTheBay.ChartWindow"

   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

   xmlns:sys="clr-namespace:System;assembly=mscorlib"

   xmlns:collections="clr-namespace:System.Collections;assembly=mscorlib"

   xmlns:charting="clr-namespace:System.Windows.Controls.DataVisualization.Charting;assembly=System.Windows.Controls.DataVisualization.Toolkit"

   xmlns:local="clr-namespace:U2UConsult.DockOfTheBay"

   Title="Data Visualization Sample"

   Icon="/DataVisualizationSample;component/dotbay.png">

    <Grid>

        <charting:Chart

           Title="Starship Dimensions" >

            <charting:Chart.Series>

                <charting:BubbleSeries

                   Title="USS Enterprise"

                   ItemsSource="{x:Static local:Enterprise.GetAll}"

                   DependentValueBinding="{Binding Length}"

                   IndependentValueBinding="{Binding Name}"

                   SizeValueBinding="{Binding CrewSize}" />

            </charting:Chart.Series>

            <charting:Chart.Axes>

                <charting:CategoryAxis

                   Orientation="X"

                   Title="Registration Number" />

                <charting:LinearAxis

                   Orientation="Y"

                   Title="Length in meter"

                   Maximum="800"

                   Minimum="0" />

            </charting:Chart.Axes>

        </charting:Chart>

    </Grid>

</Window>

 

If you also want to let the chart display a fourth property (say CommandingOfficer), then you should customize the template for the bubble itself. Unfortunately this will more than double the codebase. There is no TooltipValueBinding property or a stable style or template to inherit from (yet?). So if you want a customized tooltip then you have to override/duplicate the whole template. The safest option is to boldly copy/paste from the samples. First create a style, e.g. in Grid.Resources:

<Grid.Resources>

    <Style

       x:Key="CustomBubbleDataPointStyle"

       TargetType="charting:BubbleDataPoint" >

        <!-- Pretty background brush -->

        <Setter Property="Background">

            <Setter.Value>

                <RadialGradientBrush>

                    <RadialGradientBrush.RelativeTransform>

                        <TransformGroup>

                            <ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="2.09" ScaleY="1.819"/>

                            <TranslateTransform X="-0.425" Y="-0.486"/>

                        </TransformGroup>

                    </RadialGradientBrush.RelativeTransform>

                    <GradientStop Color="#FF9DC2B3"/>

                    <GradientStop Color="#FF1D7554" Offset="1"/>

                </RadialGradientBrush>

            </Setter.Value>

        </Setter>

        <!-- Template with custom ToolTip -->

        <Setter Property="Template">

            <Setter.Value>

                <ControlTemplate TargetType="charting:BubbleDataPoint">

                    <Grid>

                        <Ellipse

                   Fill="{TemplateBinding Background}"

                   Stroke="{TemplateBinding BorderBrush}"

                   StrokeThickness="{TemplateBinding BorderThickness}"/>

                        <Ellipse>

                            <Ellipse.Fill>

                                <LinearGradientBrush>

                                    <GradientStop

                                       Color="#77ffffff"

                                       Offset="0"/>

                                    <GradientStop

                                       Color="#00ffffff"

                                       Offset="1"/>

                                </LinearGradientBrush>

                            </Ellipse.Fill>

                        </Ellipse>

                        <ToolTipService.ToolTip>

                            <StackPanel>

                                <TextBlock Text="{Binding Name}" FontWeight="Bold" />

                                <StackPanel Orientation="Horizontal">

                                    <TextBlock Text="Length: " />

                                    <ContentControl Content="{TemplateBinding FormattedDependentValue}"/>

                                    <TextBlock Text=" meter" />

                                </StackPanel>

                                <StackPanel Orientation="Horizontal">

                                    <TextBlock Text="Crew: " />

                                    <ContentControl Content="{TemplateBinding Size}"/>

                                </StackPanel>

                                <StackPanel Orientation="Horizontal">

                                    <TextBlock Text="Commanding Officer: " />

                                    <TextBlock Text="{Binding CommandingOfficer}"/>

                                </StackPanel>

                            </StackPanel>

                        </ToolTipService.ToolTip>

                    </Grid>

                </ControlTemplate>

            </Setter.Value>

        </Setter>

    </Style>

</Grid.Resources>

 

To make use of this style, just add this to the chart's properties:

DataPointStyle="{StaticResource CustomBubbleDataPointStyle}"

As you see the charting controls in both WPF and Siverlight toolkits allow you to rapidly embed powerful charting features into your application. To be honest: it actually took me more time to collect sample data than to build these chartsWink.


Validation in a WPF DataGrid

In this article I will describe two options to implement validation on the cells and rows of a WPF DataGrid. On the architectural level you have indeed (at least) two options when it comes to validation. You can decide to validate through independent reusable logic captured in ValidationRules, or you can let the bound entities carry out the validation themselves. Let's first set up a mini-application, with a WPF DataGrid displaying a list of instances of a WeatherForecast class. It should look like this:

Here's the WeatherForecast class:

namespace U2UConsult.DockOfTheBay

{

    using System;

    using System.Collections.ObjectModel;

    using System.ComponentModel;

 

    /// <summary>

    /// Intergalactic Weather Forecast

    /// </summary>

    public class WeatherForecast

    {

        /// <summary>

        /// Gets the list of weather forecasts for tomorrow.

        /// </summary>

        public static ObservableCollection<WeatherForecast> TomorrowsForecast

        {

            get

            {

                return new ObservableCollection<WeatherForecast>()

                {

                    new WeatherForecast()

                    {

                        Planet = "Alderaan",

                        Conditions = "Risk of a severe lightning bolt",

                        LowestTemp = 0,

                        HighestTemp = 0

                    },

                    new WeatherForecast()

                    {

                        Planet = "Caprica",

                        Conditions = "Acid rain - toasters better stay inside",

                        LowestTemp = 24,

                        HighestTemp = 28

                    },

                    new WeatherForecast()

                    {

                        Planet = "Earth",

                        Conditions = "Globally warming"

                        LowestTemp = 6,

                        HighestTemp = 9

                    },

                    new WeatherForecast()

                    {

                        Planet = "Middle Earth",

                        Conditions = "Cloudy over Mordor",

                        LowestTemp = 24,

                        HighestTemp = 28

                    },

                    new WeatherForecast()

                    {

                        Planet = "Tatooine",

                        Conditions = "Heavy sand storms",

                        LowestTemp = 3,

                        HighestTemp = 8

                    }

                };

            }

        }

 

        /// <summary>

        /// Gets or sets the name of the planet.

        /// </summary>

        public string Planet { get; set; }

 

        /// <summary>

        /// Gets or sets the description of the weather conditions.

        /// </summary>

        public string Conditions { get; set; }

 

        /// <summary>

        /// Gets or sets the lowest temperature during an interval.

        /// </summary>

        /// <remarks>Unit measure is °C.</remarks>

        public int LowestTemp { get; set; }

 

        /// <summary>

        /// Gets or sets the highest temperature during an interval.

        /// </summary>

        /// <remarks>Unit measure is °C.</remarks>

        public int HighestTemp { get; set; }

    }

}

 

And here's the XAML for the form:

<Window x:Class="U2UConsult.DockOfTheBay.DataGridValidationSampleWindow"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:toolkit="http://schemas.microsoft.com/wpf/2008/toolkit"

    xmlns:local="clr-namespace:U2UConsult.DockOfTheBay"

    Icon="/DataGridValidationSample;component/dotbay.png"

    SizeToContent="WidthAndHeight"

    Title="DataGrid Validation Sample" >

    <StackPanel>

        <!-- Title -->

        <TextBlock

           Text="Intergalactic Weather Forecast"

           HorizontalAlignment="Center"

           FontWeight="Bold"

           Margin="5" />

        <!-- Weather Forecasts Grid -->

        <toolkit:DataGrid

            ItemsSource="{x:Static local:WeatherForecast.TomorrowsForecast}"

            AutoGenerateColumns="False"

            RowHeaderWidth="16">

            <toolkit:DataGrid.Columns>

                <toolkit:DataGridTextColumn

                   Header="Planet"

                   Binding="{Binding Path=Planet}"/>

                <toolkit:DataGridTextColumn

                   Header="Conditions"

                   Binding="{Binding Path=Conditions}"/>

                <toolkit:DataGridTextColumn

                   Header="Low Temperature"

                   Binding="{Binding Path=LowestTemp}"/>

                <toolkit:DataGridTextColumn

                   Header="High Temperature"

                   Binding="{Binding Path=HighestTemp}"/>

            </toolkit:DataGrid.Columns>

        </toolkit:DataGrid>

    </StackPanel>

</Window>

 

1. Validation through ValidationRules

A first way to perform validation is to store the rules in ValidationRule classes, and let the column or row bindings call that business logic at the appropriate moment (that moment is determined by the ValidationStep property of the rule). The bound entities themselves are unaware of this process.

Cell validation

You can build validation rule classes to validate a single value, e.g. a Temperature. First we check whether the input is a whole number (type check). Then we perform a range check to verify if the entered value is within the physical boundaries of temperature i.e. between absolute cold and absolute hot (no need to check the latter, because it's higher than Integer32.MaxValue anyway). Here's how this could look like:

namespace U2UConsult.DockOfTheBay

{

    using System.Windows.Controls;

 

    /// <summary>

    /// Validates a temperature in °C.

    /// </summary>

    internal class TemperatureValidationRule : ValidationRule

    {

        /// <summary>

        /// Validates the proposed value.

        /// </summary>

        /// <param name="value">The proposed value.</param>

        /// <param name="cultureInfo">A CultureInfo.</param>

        /// <returns>The result of the validation.</returns>

        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)

        {

            if (value != null)

            {

                int proposedValue;

                if (!int.TryParse(value.ToString(), out proposedValue))

                {

                    return new ValidationResult(false, "'" + value.ToString() + "' is not a whole number.");

                }

 

                if (proposedValue < -273)

                {

                    // Something was wrong.

                    return new ValidationResult(false, "Temperature can not be below the absolute cold (-273°C).");

                }

            }

 

            // Everything OK.

            return new ValidationResult(true, null);

        }

    }

}

 

All you need to do to trigger the validation, is registering the validation rules in the binding of the corresponding bound columns, like this:

<toolkit:DataGridTextColumn Header="High Temperature">

    <toolkit:DataGridTextColumn.Binding>

        <Binding Path="HighestTemp">

            <!-- Validating through independent rules -->

            <Binding.ValidationRules>

                <local:TemperatureValidationRule />

            </Binding.ValidationRules>

        </Binding>

    </toolkit:DataGridTextColumn.Binding>

</toolkit:DataGridTextColumn>

 

If you run the application in this stage, you'll notice that the validation is executed. The invalid cell is presented through the default ErrorTemplate (a red border around the contents):

You have no idea what went wrong, until you add you own error template. Let's display an icon in the row header, with the error message as tooltip:

<!-- Validation Error Template for a DataGrid Row -->

<Style TargetType="{x:Type toolkit:DataGridRow}">

    <Setter Property="ValidationErrorTemplate">

        <Setter.Value>

            <ControlTemplate>

                <Image

                   Source="/DataGridValidationSample;component/error.png"

                   ToolTip="{Binding RelativeSource={

                                         RelativeSource FindAncestor,

                                         AncestorType={x:Type toolkit:DataGridRow}},

                                     Path=(Validation.Errors)[0].ErrorContent}"

                   Margin="0"

                   Width="11" Height="11" />

            </ControlTemplate>

        </Setter.Value>

    </Setter>

</Style>

 

Now you get all the details when the validation rule is broken:

 

Row validation

WPF 3.5 SP1 introduced the BindingGroup class. A BindingGroup represents a set of related Bindings -e.g. the bindings to different properties of the same entity. A BindingGroup can also span bindings to multiple entities -e.g. a list of travellers reserving a group ticket. Here's a validation rule that validates a WheaterForecast:

namespace U2UConsult.DockOfTheBay

{

    using System.Windows.Controls;

    using System.Windows.Data;

 

    /// <summary>

    /// Validation of WeatherForecast instances.

    /// </summary>

    public class WeatherForecastValidationRule : ValidationRule

    {

        /// <summary>

        /// Validate a whole collection of binding sources.

        /// </summary>

        /// <param name="value">A BindingGroup.</param>

        /// <param name="cultureInfo">A CultureInfo.</param>

        /// <returns>The result of the validation.</returns>

        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)

        {

            // Get binding group.

            BindingGroup bindingGroup = value as BindingGroup;

 

            if (bindingGroup != null)

            {

                // Loop through binding sources - could be multiple.

                foreach (var bindingSource in bindingGroup.Items)

                {

                    // Get object.

                    WeatherForecast forecast = bindingSource as WeatherForecast;

 

                    // Validate object.

                    if (forecast.HighestTemp < forecast.LowestTemp)

                    {

                        // Something was wrong.

                        return new ValidationResult(false, "Why do you think we called it 'High' and 'Low' ?");

                    }

                }

            }

 

            // Everything OK.

            return ValidationResult.ValidResult;

        }

    }

}

 

Note: this class is very tightly coupled to the WeatherForecast class itself.

The validation rule is called from the datagrid's bindings via RoleValidationRules:

<!-- Validating through independent rules -->

<toolkit:DataGrid.RowValidationRules>

    <local:WeatherForecastValidationRule ValidationStep="ConvertedProposedValue" />

</toolkit:DataGrid.RowValidationRules>

 

The Error Template that we defined earlier in this article, can be reused for row validation: 

2. Validation through IDataErrorInfo

A second way to perform validation is to store all the necessary business logic inside the entity, and expose it through the IDataErrorInfo interface. A DataErrorValidationRule at column and row level will call the methods on this interface.

Cell validation

An indexer with the property name as parameter will be called if a single property needs to be validated. Here's the code for the WeatherForecast class:

/// <summary>

/// Gets the result of a validation at Property level (from IDataErrorInfo).

/// </summary>

/// <param name="columnName">Name of the modified property.</param>

/// <returns>A validation error message.</returns>

/// <remarks>Called by DataErrorValidationRule at Column level.</remarks>

public string this[string columnName]

{

    get

    {

        // Temperature range checks.

        if ((columnName == "LowestTemp") || (columnName == "HighestTemp"))

        {

            if ((this.LowestTemp < -273) || (this.HighestTemp < -273))

            {

                return "Temperature can not be below the absolute cold (-273°C).";

            }

        }

 

        // All validations passed successfully.

        return null;

    }

}

 

An alternative is to do the validation in the setter of the properties, and only define an ExceptionValidationRule at column level.

Row validation

A DataErrorValidationRule can also be instantiated from the RowValidationRules of the DataGrid:

<!-- Validating through IDataErrorInfo -->

<toolkit:DataGrid.RowValidationRules>

    <DataErrorValidationRule />

</toolkit:DataGrid.RowValidationRules>

 

It will call the Error property on the bound entities:

/// <summary>

/// Gets the result of a validation at Entity level (from IDataErrorInfo).

/// </summary>

/// <remarks>Called by DataErrorValidationRule at Row level.</remarks>

public string Error

{

    get

    {

        // Compare temperatures.

        if (this.LowestTemp > this.HighestTemp)

        {

            return "Why do you think we called it 'High' and 'Low' ?";

        }

 

        // All validations passed successfully.

        return null;

    }


A minimalistic template for an editable WPF DataGrid

By default, a WPF DataGrid operates in its birthday suit, with no decorations at all. For your end user this is not intuitive, so you should provide some fig-leafs here and there. In my humble opinion, at least two rows should be easily identifiable in any editable grid:

  • the 'new' row (generally at the bottom of the grid), and
  • the row that is currently being edited.

Both rows deserve their own template. As you certainly know, you can get very far with WPF templating. In this article however I'll go for the minimalistic approach and display

  • an asterisk in front of the 'new' row, and
  • a pencil in front of the row in edit mode.

An asterisk for the 'New' row

This is what we're trying to achieve:

We will get this result by applying a data template to the row's header. So start with a data template in XAML, somewhere in a resource element:

<DataTemplate x:Key="AsteriskTemplate" >

    <TextBlock

       Margin="0"

       HorizontalAlignment="Center"

       VerticalAlignment="Center"

       ToolTip="New"

       Text="*">

    </TextBlock>

</DataTemplate>

 

This template should be applied when loading the row, so hook an event handler to LoadingRow, also in XAML:

<toolkit:DataGrid

   x:Name="DriversDataGrid"

   ItemsSource="{Binding Source={x:Static local:FormulaOneDriver.GetAll}}"

   CommandManager.PreviewExecuted="DriversDataGrid_PreviewDeleteCommandHandler"

   LoadingRow="DriversDataGrid_LoadingRow"

   RowEditEnding="DriversDataGrid_RowEditEnding"

 

Here's the C# code for the event handler:

/// <summary>

/// Apply Asterisk DataTemplate to New Row

/// </summary>

/// <param name="sender">Sender of the event: the DataGrid.</param>

/// <param name="e">Event arguments.</param>

private void DriversDataGrid_LoadingRow(object sender, DataGridRowEventArgs e)

{

    if (e.Row.Item == CollectionView.NewItemPlaceholder)

    {

        e.Row.HeaderTemplate = (DataTemplate)DriversDataGrid.FindResource("AsteriskTemplate");

        e.Row.UpdateLayout();

    }

}

A pencil for the 'Edit' row

This is what we're trying to achieve: 

Again we define the look of the row header through a data template:

<DataTemplate x:Key="PencilTemplate" >

    <Image Source="/DataGridSample;component/pencil.png"

          Margin="0"

          Height="11" Width="11"

          HorizontalAlignment="Center" VerticalAlignment="Center"

           />

</DataTemplate>

 

The template is applied when we enter edit-mode. So you should handle the BeginningEdit event. When leaving edit mode, don't forget to revert to the default template, through the RowEditEnding event:

<toolkit:DataGrid

   x:Name="DriversDataGrid"

   ItemsSource="{Binding Source={x:Static local:FormulaOneDriver.GetAll}}"

   CommandManager.PreviewExecuted="DriversDataGrid_PreviewDeleteCommandHandler"

   LoadingRow="DriversDataGrid_LoadingRow"

   BeginningEdit="DriversDataGrid_BeginningEdit"

   RowEditEnding="DriversDataGrid_RowEditEnding"

 

Here's the event handler to apply the template:

/// <summary>

/// A row goes in edit mode. So display the pencil in its header.

/// </summary>

/// <param name="sender">Sender of the event: the DataGrid.</param>

/// <param name="e">Event arguments.</param>

private void DriversDataGrid_BeginningEdit(object sender, DataGridBeginningEditEventArgs e)

{

    e.Row.HeaderTemplate = (DataTemplate)DriversDataGrid.FindResource("PencilTemplate");

    e.Row.UpdateLayout();

}

 

And here's how to remove the template:

/// <summary>

/// A row leaves edit mode: persist it, and revert to the default header.

/// </summary>

/// <param name="sender">Sender of the event: the DataGrid.</param>

/// <param name="e">Event arguments.</param>

private void DriversDataGrid_RowEditEnding(object sender, DataGridRowEditEndingEventArgs e)

{

    [...]

    e.Row.HeaderTemplate = null;

}


Receive the U2U Newsletter. Submit your email address:
 
 


 


Search

rss  RSS

Tags

None

    Blogroll