WCF Data Services 4.0 in less than 5 minutes 08 April 2010 Diederik-Krols .NET 4.0, WPF, Entity Framework 4.0 WCF Data Services 4.0 (formerly known as ADO.NET Data Services, formerly known as Astoria) is one of the ways to expose an Entity Data Model from Entity Framework 4.0 in a RESTful / OData way. This article explains how to create such a data service and how to consume it with a browser and with a WPF client. The Data Service Start with an empty ASP.NET Application: Add a WCF Data Service to it: Also add an Entity Data Model to the ASP.NET project: Follow the Model Wizard to create a model containing entities on top of the Employee and Person tables from the AdventureWorks2008 database: In the designer, you should have something like this: A lot of code was generated, let's add our own 50ct in the service's code behind. First let it inherit from DataService<AdventureWorks2008Entities>: public class WcfDataService : DataService<AdventureWorks2008Entities> { .. } Then modify the InitializeService method as follows. This exposes all operations and grants all access rights (not really a production setting): public static void InitializeService(DataServiceConfiguration config) { config.SetEntitySetAccessRule("*", EntitySetRights.All); config.SetServiceOperationAccessRule("*", ServiceOperationRights.All); config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2; } Believe it or not, we're done (for the first part): the entity model is now exposed in a RESTful way. At the root URL you get an overview of the exposed entities. In the attached sample the root URL is "http://localhost:1544/WcfDataService.svc", but you may of course end up with another port number: At the "/Employees" address you find all employees: In your browser this list of employees may appear like this: This means it's time to -at least temporarily- disable your rss feed reading view. Here's how to do this in IE: To reach an individual entity, just type its primary key value in parentheses at the end of the URL, like "http://localhost:1544/WcFDataService.svc/Employees(1)": You can navigate via the relationships between entities. This is how to reach the Person entity, connected to the first Employee. The URL is "http://localhost:1544/WcfDataService.svc/Employees(1)/Person": Other OData URI options options can be found here, including: Filtering: http://localhost:1544/WcfDataService.svc/Employees?$filter=JobTitle eq 'Chief Executive Officer' Projection: http://localhost:1544/WcfDataService.svc/Employees?$select=JobTitle,Gender Client side paging: http://localhost:1544/WcfDataService.svc/Employees?$skip=5&$top=2 Version 4.0 also includes support for server side paging. This gives you some control over the resources. Add the following line in the InitializeService method: config.SetEntitySetPageSize("Employees", 3); Only 3 employees will be returned now, even if the client requested all: A Client Enough XML for now. WCF Data Services also expose a client side model that allows you to use LINQ. Create a new WPF application: Add a Service Reference to the WFC Data Service: Decorate the Window with two buttons and a listbox. It should look more or less like this: The ListBox will display Employee entities through a data template (OK, that's XML again): <ListBox Name="employeesListBox" ItemTemplate="{StaticResource EmployeeTemplate}" Margin="4" Grid.Row="1"/> Here's the template. It not only binds to Employee properties, but also to Person attributes: <DataTemplate x:Key="EmployeeTemplate"> <StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Path=Person.FirstName}" FontWeight="Bold" Padding="0 0 2 0"/> <TextBlock Text="{Binding Path=Person.LastName}" FontWeight="Bold"/> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text="{Binding Path=JobTitle}" Width="180"/> <TextBlock Text="{Binding Path=VacationHours}" Width="60" TextAlignment="Right" /> <TextBlock Text=" vacation hours taken." /> </StackPanel> </StackPanel> </DataTemplate> The Populate-Button fetches some Employee entities together with their related Person entity, and binds the collection to the ListBox (in version 4.0 two-way bindings are supported for WPF): private void Populate_Click(object sender, RoutedEventArgs e) { AdventureWorks2008Entities svc = new AdventureWorks2008Entities( new Uri("http://localhost:1544/WcfDataService.svc")); this.employeesListBox.ItemsSource = svc.Employees.Expand("Person").Where(emp => emp.BusinessEntityID < 100); } Here's the result: The Update-Button updates the number of vacation hours of the company's CEO. It fetches the Employee, updates its VacationHours property, then tells the state manager to update the employee's state, and eventually persists the data: private void Update_Click(object sender, RoutedEventArgs e) { AdventureWorks2008Entities svc = new AdventureWorks2008Entities( new Uri("http://localhost:1544/WcfDataService.svc")); Employee employee = svc.Employees.Where(emp => emp.BusinessEntityID == 1).First(); employee.VacationHours++; svc.UpdateObject(employee); svc.SaveChanges(); } If you now repopulate the listbox, you will see the increased value: Source Code Here's the full source code of this sample (just requires VS2010 with no extra downloads): U2UConsult.WcfDataServices.Sample.zip (96,59 kb) Enjoy!
PRISM in 600 seconds 31 March 2010 Diederik-Krols WPF Welcome to the lean, mean, no Vicodin, U2U Consult PRISM machine. (595 seconds left.) CompositeWPF, or Composite Application Guidance (CAG) including Composite Application Library (CAL) is still commonly referred to as PRISM. The software component -CAL- extends Windows Presentation Foundation (WPF) with Modularity, Dependency Injection, Loosely Coupled Events, Distributed Commanding, and Run-Time Module Discovery, while leveraging the classic WPF principles and design concepts such as Dependency Properties, Routed Events, Commanding, Data Binding and Data Templates. In a composite application, functionality is embedded in small parts called modules. Each module is an autonomous, dynamically discoverable and loadable piece, containing its own view, logic and services. A module is preferably designed and implemented according to the MVP-pattern. The main application contains one or more shells: empty windows with regions that contain the placeholders for the module's views. This article walks through a small sample CAL-based solution. Here's how the GUI looks like, in sober black and white: If you would build it as a single monolithic application, it would take you 10 minutes to develop. The CAL-based implementation is actually composed of 6 (six) independent projects with hardly any references to one another: And now 10 minutes is maybe even too long to explain how everything works. Anyway, I'm going to give it a try! Shells and Regions A Composite WPF application starts as a regular WPF application with references to all of the CAL-assemblies (the ones starting with Microsoft in the next screen shot): The CompositeWPF namespace is imported into the XAML of the Main Window: xmlns:cal="http://www.codeplex.com/CompositeWPF" Regions are added in the XAML by creating ItemControl and ContentControl instances with a RegionName. This dependency property exposes the region's name to CAL's region manager: <ItemsControl x:Name="MainRegion" cal:RegionManager.RegionName="MainRegion" /> Modules and Dependency Injection The AboutModule is kind of a "Hello World" module: it just statically displays some company's logo. A module generally doesn't need to call the whole CAL-shebang, so it can get away with less references: The module itself is a class that implements the IModule interface. It is decorated with the Module atttribute to allow dynamic discovery: [Module(ModuleName = "AboutModule")] public class AboutModule : IModule { // ... } The graphical part of the module is implemented as a WPF UserControl and saved in a Views subfolder. It needs no special references or interfaces: The IRegionManager parameter in the module's constructor will be discovered and populated by CAL's Dependency Injection Container (Unity, that is). It gives the module and the view access to the shell's region manager: public AboutModule(IRegionManager regionManager) { this.regionManager = regionManager; } CAL will call the module's constructor, and then its Initialize method, where the view is registered in the named region: public void Initialize() { this.regionManager.RegisterViewWithRegion( "MainRegion", typeof(Views.U2UConsultLogo)); } Bootstrapping the Application The main project contains a bootstrapper. It makes sure that CAL's components are properly initialized before anything else happens: internal class Bootstrapper : UnityBootstrapper { // ... } The OnStartUp of the application is overridden: protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); Bootstrapper bootstrapper = new Bootstrapper(); bootstrapper.Run(); } Since the bootstrapper will do the initialization, the StartupUri is removed from app.xaml: <Application x:Class="U2UConsult.CAL.Sample.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <!--StartupUri="Shell.xaml"--> </Application> Back in the bootstrapper, the CreateShell() method is overridden to make sure that the shell gets loaded and displayed: protected override DependencyObject CreateShell() { Shell shell = new Shell(); shell.Show(); return shell; } The GetModuleCatalog() method is also overridden. It returns the collection of discovered modules. There are different types of module catalog subclasses that can inspect directories, configuration files, or even XAML files. The following code is an example of using the ModuleCatalog base class itself. This way of statically loading modules requires a reference from the shell project to the module project(s), so it's only used while developing/debugging: protected override IModuleCatalog GetModuleCatalog() { ModuleCatalog catalog = new ModuleCatalog(); catalog.AddModule(typeof(AboutModule.AboutModule)); return catalog; } Model View Presenter The region at the bottom of the main window's shell displays the application's status. XAML-wise it's just a textblock. When it is is implemented using a (lightweight) MVP-pattern and packaged as a CAL module, it looks rather impressive: The view contains a textblock which is bound to a property in the PresentationModel: <TextBlock TextWrapping="NoWrap" Text="{Binding StatusMessage}"/> The StatusModule requires more of the CAL feature set than the AboutModule, so it has more injected stuff (i.e. parameters in its constructor): public StatusModule( IUnityContainer container, IRegionManager regionManager, IEventAggregator eventAggregator) { this.container = container; this.regionManager = regionManager; this.eventAggregator = eventAggregator; } The registration of the views and services that are provided by the module, is nicely factored out to a seperate method: public void RegisterViewsAndServices() { this.container.RegisterType<Views.IStatusView, Views.StatusView>( new ContainerControlledLifetimeManager()); this.container.RegisterType<PresentationModels.IStatusPresentationModel, PresentationModels.StatusPresentationModel>( new ContainerControlledLifetimeManager()); } And here's the module's Initialize-method: public void Initialize() { this.RegisterViewsAndServices(); PresentationModels.IStatusPresentationModel model = this.container.Resolve<PresentationModels.StatusPresentationModel>(); IRegion statusRegion = this.regionManager.Regions["StatusRegion"]; statusRegion.Add(model.View); } The PresentationModel gets its view from Dependency Injection, again through a parameter in its constructor. So here's where the model and its view are connected: public StatusPresentationModel(Views.IStatusView view) { this.view = view; view.Model = this; this.StatusMessage = "The status module is operational."; } Distributed Events The SelectionModule allows the end user to select an option, after which it will update the application's status. This status is displayed in the status region at the bottom of the shell: Some corners were cut here: the project only contains a Module and a View. Anyway, the SelectionModule needs to communicate with the StatusModule. Both modules share an event -an instance of CompositePresentationEvent- through CAL's event aggregator, a mechanism for distributed events. The reference to this event aggregator comes from ... Dependency Injection indeed: the constructors of SelectionModule, SelectionView, StatusModule, and StatusPresentationModel take an IEventAggrator parameter. A global StatusReported event is declared in the application. The SelectionModule raises this event via a call to Publish(): private void RadioButton_Checked(object sender, RoutedEventArgs e) { this.selectedOption = e.Source as RadioButton; string status = string.Format( "You selected '{0}'.", this.selectedOption.Content.ToString()); this.eventAggregator.GetEvent<Infrastructure.StatusReportedEvent>().Publish(status); } The StatusModule has a local event handler for the same event: private void OnAppStatusChanged(string message) { this.StatusMessage = message; } It registers that handler with CAL through a Subscribe-call: this.eventAggregator.GetEvent<Infrastructure.StatusReportedEvent>() .Subscribe(this.OnAppStatusChanged, ThreadOption.UIThread, true); The handler itself is straightforward - data binding will update the view: private void OnAppStatusChanged(string message) { this.StatusMessage = message; } As you observed, the reference to the event is not by name (like for a region), but it is made by type. Both modules, as well as all other modules that want to notify status changes, need to know the event type: public class StatusReportedEvent : CompositePresentationEvent<string> { } The type is defined is a so-called Infrastructure project to which multiple modules -and probably even the shell- will have a reference. To call a spade a bloody shovel: GlobalVariables would be a more appropriate name for this assembly. But that name seems to upset software architects. Composite Commands In WPF we like to communicate through Commands, not Events. So here's the next use case. The shell has a toolbar region on top. This will host the toolbar buttons of each individual module, but also some global commands. A global "Clear" button clears any registered view, and updates the status: The global command itself is an instance of CAL's CompositeCommand class - a command that can have child commands. It is defined in the Infrastructure project, together with a proxy to it that can be overridden for unit testing: namespace Infrastructure { using Microsoft.Practices.Composite.Presentation.Commands; public static class GlobalCommands { public static CompositeCommand ClearCommand = new CompositeCommand(); } public class ClearCommandProxy { public virtual CompositeCommand ClearCommand { get { return GlobalCommands.ClearCommand; } } } } The button with the large "X" is linked to the global command through its XAML: <UserControl x:Class="GlobalCommandsModule.Views.SelectionCommandView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:inf="clr-namespace:Infrastructure;assembly=Infrastructure" > <Button Command="{x:Static inf:GlobalCommands.ClearCommand}" ... /> </UserControl> When the SelectionView is loaded, it hooks a DelegateCommand to the global ClearCommand: public SelectionView( IEventAggregator eventAggregator, ClearCommandProxy commandProxy) { InitializeComponent(); this.eventAggregator = eventAggregator; this.clearCommand = new DelegateCommand<object>(this.Clear, this.CanClear); commandProxy.ClearCommand.RegisterCommand(this.clearCommand); } DelegateCommand is yet another CAL-class. It lets you directly specify the ICommand-code for Execute and CanExecute as delegates. Dynamic Module Lookup During development it makes sense to keep a reference from the host application to each module, and load it statically into the module catalog. At the end of the day, it's better to cut that reference and go for dynamic lookup. So all modules are compiled and their dll's gathered in a "Modules"-folder: The "production"-bootstrapper makes use of one of the more specialized catalog modules: protected override IModuleCatalog GetModuleCatalog() { DirectoryModuleCatalog catalog = new DirectoryModuleCatalog(); catalog.ModulePath = "../../Modules"; return catalog; } Source Code Here's the full source code of the sample application: U2UConsult.CAL.Sample.zip (1,79 mb) Enjoy!
WPF Browser Control Battle: Chromium versus Internet Explorer 28 February 2010 Diederik-Krols WPF Did you ever want to embed a browser in a WPF application, to host a help file or to access an intranet site? Did you ever want to impress your girl friend by building a fancy browser yourself, like YouCube? If so, your obvious choice would be to drag and drop a native WebBrowser Control into your application. Well, hold your horses (or rather: hold your mouse). The standard WPF WebBrowser control that sits in your Visual Studio toolbox is just an ultra-thin wrapper around Microsoft's Internet Explorer ActiveX. This wrapper is so thin that it's not even aware of such celebrated WPF features as transformations and styles. If you use the control in any container that is just a little bit dynamic, then it will simply go berserk. Here's an example. I wrapped a native WebBrowser control in a ScrollViewer, then applied some gentle transformations. The browser just walks happily over the entire GUI: So if you're planning to build a user experience like in the following screenshot (which is the already mentioned YouCube), then forget about Internet Explorer, and start looking for an alternative: Ingredients The alternative solution basically requires 3 ingredients: a browser, obviously, an SDK to get programmatic access to the browser's core functionality, and a WPF wrapper that does more than just adding a WindowsFormsHost element. A Browser An interesting alternative comes from an unexpected source:Google. Chromium is the open source browser on which Google Chrome is based. It's unbranded, it doesn't automatically update, and it doesn't send crash reports and usage statistics to the Googleplex (all details here). Cool, that's just what we want in our application! An SDK Awesomium is a platform independent C++ software library that allows developers to embed Chromium in their own applications. Older versions were released as open source, but the code is currently being productized. A real WPF wrapper Chris Cavanagh built WPF Chromium, which exposes Awesomium as a .NET and WPF control. Feel free to check the Sources and References. To use WPF Chromium's WebBrowser control in your own application, add a reference to the .NET dll's: You also need to make sure that the (non-.NET) Awesomium dll's end up next to your executable: Finally, don't forget to compile in 32-bit mode: My guess: we now have a serious challenger for IE. Battle of the Browsers I built a little torture chamber to see how far we can stretch the native IE browser and WPF Chromium. The sample application straps a browser in a ScrollViewer and tries to shake it around with transformations. It also implements scrollwheel-zooming and left-button panning, and last but not least: it tries to override the scrollbar style. Here are some observations: Functionality Both browsers behave ... like a browser. Both were tested against search engines, sites with long pages -like this blog-, sites with lots of images, and sites with silverlight, flash, or Ajax. Chromium is better and faster on sites with complex javascripts (e.g. Ajax sites). An illustration: IE continuously throws errors in your face if you visit this site: On the other hand, IE is a much better Silverlight host. When you navigate to this beautiful but technically rather simple Silverlight site, WPF Chromium -or the underlying Awesomium- experiences problems in tracking the mouse position: This is as good as it gets for the Internet Explorer ActiveX. For the rest of the article, you may ignore it: the control browses but don't expect anything else from it. And by the way: there's no improvement on the horizon: WPF 4.0 still has the same IE ActiveX wrapper. Transformations A small test with some slider controls rapidly reveals that transformations are not an issue for WPF Chromium. Here's an example: Zooming via a slider control is not so sexy; so I implemented zooming through the scrollwheel. Works fine. And now that we're talking about the scrollwheel: both browsers support scrollwheel panning. Unfortunately Chromium doesn't show the cursors (that's a weird user experience). So just for fun I implemented left-button panning. By holding the left button and moving the cursor, you can drag the underlying bitmap around. It's a nice replacement of scrolling, but the first symptoms of overstressing the control appeared: you need to specify a fixed size for the displayed page (continue reading for more details). Styling There's not much to style in a Browser control: it just renders a bitmap, right ? Wrong ! To offer you the pleasure of scrolling, a browser calculates the estimated size of the rendered page. It then displays its own set of native Windows GDI scroll bars . You can't override these scrollbars, but you can hide them, and wrap the browser control in a -stylable- WPF ScrollViewer control. You need to know the estimated size of the underlying (virtual) bitmap, which unfortunately is not accessible. As a work around set it to fixed size. If you set it too small, you can only scroll through a part of the site, but setting it too large will horribly slow down the scrolling experience. Here's an example of styled scrollbars (remember: de gustibus et coloribus non est disputandum also applies to styles): Source Code Here it is, a demo of Chromium and Awesomium, by U2UConsultium : U2UConsult.ChromiumBrowser.Sample.zip (9,09 mb) Enjoy !
Yet another Circular ProgressBar control for WPF 27 February 2010 Diederik-Krols WPF Here is (yet another) WPF circular busy indicator control based on Sacha Barber's Circular Progress Bar. This type of control is useful to indicate that a part of your user interface is waiting for the result of an asynchronous call. The user control is hosts a Canvas in which 9 circles are displayed, with decreasing opacity. A rotation transformation produces the spinning effect. Here's its default look: The canvas is embedded in a Viewbox for easy resizing. To keep the circles round, the control is kept square (you might want to read this again ;-). This is done by binding its Width to its Height. It's a two-way binding, so you can set either property to set the control's size: Width="{Binding RelativeSource={RelativeSource Self}, Path=Height, Mode=TwoWay}" The CPU-consuming rotation is only triggered when the Visibility is set to Visible. The control keeps a low GUI profile: it is by default invisible, its background is transparent, it even has a zero opacity: Visibility="Hidden" IsVisibleChanged="HandleVisibleChanged" Opacity="0" Background="Transparent" Here's the code that is executed when the Visibility changes: /// <summary> /// Visibility property was changed: start or stop spinning. /// </summary> /// <param name="sender">Sender of the event.</param> /// <param name="e">Event arguments.</param> private void HandleVisibleChanged(object sender, DependencyPropertyChangedEventArgs e) { // Don't give the developer a headache. if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) { return; } bool isVisible = (bool)e.NewValue; if (isVisible) { this.StartDelay(); } else { this.StopSpinning(); } } The Fill property of the ellipses is bound to the control's Foreground property, through a Style. This is very intuitive, and requires no extra dependency properties: <Style TargetType="Ellipse"> <Setter Property="Width" Value="20" /> <Setter Property="Height" Value="20" /> <Setter Property="Stretch" Value="Fill" /> <Setter Property="Fill"> <Setter.Value> <Binding Path="Foreground"> <Binding.RelativeSource> <RelativeSource Mode="FindAncestor" AncestorType="{x:Type local:CircularProgressBar}" /> </Binding.RelativeSource> </Binding> </Setter.Value> </Setter> </Style> Remember that the Foreground property is more than just a color, it can be any type of Brush. Here are some examples: SolidBrush RadialGradientBrush ImageBrush To give you control over the spinning speed, I created a dependency property called RotationsPerMinute: /// <summary> /// Spinning Speed. Default is 60, that's one rotation per second. /// </summary> public static readonly DependencyProperty RotationsPerMinuteProperty = DependencyProperty.Register( "RotationsPerMinute", typeof(double), typeof(CircularProgressBar), new PropertyMetadata(60.0)); There is a similar dependency property that allows to to specify the delay before the control becomes visible: StartupDelay. This is actually why I needed to start with a zero Opacity. When the control becomes visible, we wait for a short while before setting its Opacity to 1. This keeps the user interface nice and easy without too briefly flashing indicators. I (re-)used the animation timer to implement the delay. This way we are sure not to freeze the user interface. To show the user that something was started, we show a wait cursor while ... waiting. When the control becomes really visible, we reset the cursor to its original image: /// <summary> /// Startup Delay. /// </summary> private void StartDelay() { this.originalCursor = Mouse.OverrideCursor; Mouse.OverrideCursor = Cursors.Wait; // Startup this.animationTimer.Interval = new TimeSpan(0, 0, 0, 0, this.StartupDelay); this.animationTimer.Tick += this.StartSpinning; this.animationTimer.Start(); } /// <summary> /// Start Spinning. /// </summary> /// <param name="sender">Sender of the event.</param> /// <param name="e">Event Arguments.</param> private void StartSpinning(object sender, EventArgs e) { this.animationTimer.Stop(); this.animationTimer.Tick -= this.StartSpinning; // 60 secs per minute, 1000 millisecs per sec, 10 rotations per full circle: this.animationTimer.Interval = new TimeSpan(0, 0, 0, 0, (int)(6000 / this.RotationsPerMinute)); this.animationTimer.Tick += this.HandleAnimationTick; this.animationTimer.Start(); this.Opacity = 1; Mouse.OverrideCursor = originalCursor; } Here's how you use the control (all properties have a default value, so it could be shorter): <local:CircularProgressBar StartupDelay="500" Foreground="SteelBlue" RotationsPerMinute="120" Width="200" /> I also built a sample client, so you can experiment with the settings. Here's how it looks like: Here's the full source code: U2UConsult.CircularProgressBar.Sample.zip (42,66 kb) Enjoy !
How to play the Accordion - WPF Toolkit 20 February 2010 Diederik-Krols WPF 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 !
Displaying spatial data in WPF: from SqlGeometry to PathGeometry 26 December 2009 Diederik-Krols WPF, SQL Server 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 }; } } } }
SQL Spatial Tools: Map Projections 25 December 2009 Diederik-Krols SQL Server, WPF 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 article). Anyway, here's the full solution: U2UConsult.DockOfTheBay.SpatialProjectionsSample.zip (144,07 kb).
An Office WPF Ribbon Control Walkthrough 29 November 2009 Diederik-Krols WPF 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 19 October 2009 Diederik-Krols WPF 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 charts.
Validation in a WPF DataGrid 30 September 2009 Diederik-Krols WPF 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; } }