BBQ Shack – Ocean v2 for Visual Studio 2008

February 7, 2010

homepage What the Heck is BBQ Shack?

In September of 2009 I went on a cruise to Alaska with a simple goal of writing a WPF application that shared business objects and Ocean framework code with a Silverlight 3 project within the solution.  The WPF and Silverlight code sharing has since been made much easier.  Since Visual Studio 2010 Beta2, Silverlight 3 and 4 can easily share code with WPF;  I blogged how to do this here.

Before this cruise Jaime Rodriguez was encouraging me to quit writing line of business applications that looked and felt like Windows Forms applications.  He wanted me to start taking advantage of the capabilities of the WPF platform and not settle with an older UI paradigm.

Additionally, I felt challenged by the WPF work Billy Hollis did in 2008.  I strongly recommend that every WPF & Silverlight line of business application developer watch this challenging dnrTV video.

To my surprise, once I got aboard ship, I realized that two suite cases of Scope Creep got dropped off in my suite.  BBQ Shack began to grow into a medium size application requiring a lot of new Ocean framework features be coded on this cruise.

BBQ Shack was inspired by a real BBQ take-out, Outlaw BBQ Shack that is located in Matthews, NC.  If you are ever in the Charlotte, NC area you must visit the Outlaw BBQ Shack.  This small family owned business provided the BBQ for the Super Bowl XLI in 2007.

The BBQ Shack application story is; a software provider writes a WPF application that is initially used for manager functions, item maintenance and touch screen cash register software.  After a few months of use, the customer asks for a web presence.  So a Silverlight ordering application is deployed.  To save time and money, code reuse was critical.  (this is our scenario, not actual events in real life)

While the above story is just a scenario, I spent a good bit of time (and had fun) creating a data model that would allow me to have a very flexible way of maintaining inventory and understanding actual costs for products sold.

My goal for this blog post and the BBQ Shack application is to demonstrate the features in Ocean v2, while at the same time provide a real world MVVM line of business example.  Trying to reign in the scope, I did not write the Sales or Order Packing menu options nor the actual checkout screens.

I wrote 90% of BBQ Shack on the week long cruise, including a simple code generator for the business objects, business and data layer code.  After getting back from the cruise, I spent time with superstars Glenn Block and Jeff Handley.  They encouraged me to go back and make BBQ Shack IOC and test friendly.  Glenn and I also knocked around on a Friday evening for a few hours and wrote BLL(Of T) and DAL(Of T).  I refined that work and integrated it into BBQ Shack and Ocean v2.

In case you’re wondering, I used Expression Blend to create the above BBQ grill in XAML.

Table of Contents

Videos

The below links are to my Vimeo videos.  I have a short blog post here, that explains Vimeo and the features it provides you as a viewer.

I recommend that you view at least the first video before reading the blog post.  This will give you context and help your understanding of the application and Ocean API features introduced here.

BBQ Shack WPF version

BBQ Shack Silverlight version

Non-linear navigation deep dive

Ocean v2

I have created a home page for Ocean here.  Ocean v1, born on an Alaskan cruise in September of 2008 is the name for the framework I use for my application development.

Ocean v2 was created on my September 2009 Alaskan cruise along with the BBQ Shack.

See Ocean’s home page for more on Ocean v3.

BBQ Shack Features

I’m going to limit this blog post to the following feature list, otherwise I would never publish this work.  The videos at the bottom of the post will talk about every feature in BBQ Shack.

  • Non-linear application navigation with transitions
  • Task switcher
  • MVVM Modal & logging services
  • Master page concept forms
  • Background loading of all data
  • Business entity validation
  • BLL(Of T)
  • DAL(Of T)
  • Silverlight Code Sharing

While not a feature, Dan Dole a co-worker at Microsoft told me about a color pallet site called kuler.  I’ve long been told my UI’s don’t look so great.  I was trying to turn out a decent UI for BBQ Shack so I searched through the many color pallets at kuler.  BBQ Shack only uses a single pallet with 5 colors, along with black, white and a light gray.  Simple turned out to be best.

Non-Linear Application Navigation – View Manager Services

Billy Hollis used the phrase, “non-linear navigation” in the PDC presentation he did that was similar to the above dnrTV video.  The application he demonstrated was a product he wrote for his customer.  He spoke about the ability to hyperlink to any form or data record from anywhere in the application. 

Visualize Dashboard that presents a list of items that are low on inventory.  The ability to navigate directly to the item or another pertinent form is invaluable.  (yes sounds simple, keep reading)

Visualize some UI that presents the user a list of all active forms with the ability to navigate back to it from anywhere in the application.

WPF and Silverlight do not ship with baked in MDI support.  In my opinion this is good because it forces developers to design much better UI’s for their customers that don’t include countless little windows.

I also see non-linear navigation as a solution for end users that are required to multi-task to perform their job function.

BBQ Shack demonstrates non-linear navigation that allows a form to have a modal dialog displayed, slide that form out of the way to allow the user to perform another tasks and then easily navigate back to that task, just like users need to do in the real world.

BBQ Shack also provides 4 different animated form transitions that can be user selectable at run-time as part of a users profile if desired.

The below image pictures the list that is displayed when the user right-clicks on the circled “4” at the top of the Inventory button.  Clicking on one of the records will navigate the user directly to the form with that record.

listnavigation

When form is slide out of the way, it is not destroyed.  In fact it is running and can continue to run processes when its out of view.  This is one reason I listed background loading of all data in the features list.  If you process data access on the UI thread, you will severely limit your applications ability to provide multi-tasking features to your users and will also have issues with UI animation if the UI thread is also performing data access functions.  Background data access and processing solves this problem.

This non-linear, non-destructive feature really opens the possibilities for Silverlight 4 line of business applications.  Not forcing user to complete a form before being able to work on another form is a great UI feature.  This Silverlight feature will I ship with Ocean v3.1 for Visual Studio 2010 with Silverlight 4 support.  I need to wait until Silverlight 4 RTM’s before shipping any Silverlight 4 code.

The below image pictures the Inventory module with 4 active screens (4 includes the one pictured).  The user can easily see which forms are active with several options for getting to each one.  Notice in both the above and below images that the form state is displayed along with the record key and record title.

acitverecords

Consumers of business application software are experts in their vertical markets and are also very busy people.  Software that makes their job easier and transforms the computer from some sort of black box with a screen to a user friendly tool should be one of the goals of modern software developers.

The above search results have a unique feature.  The text of the result is a single click hyperlink to the record.  If the user clicks to the right of the text, that row will be selected and the Edit and Delete buttons will be enabled.

The below image shows a maintenance form with a modal dialog displayed.  The parent form is slightly grayed out while the application buttons at the bottom are not grayed out.  This allows for another application function to be carried out and then return to this form at a later time.   

modalform

Notice in the modal form the Item Base label is underlined.  This indicates that this is a hyperlink that when clicked will bring the Item Base Maintenance form into view, allowing that table to be modified.  When that Item Base form is closed, the application will come back to the above form in the same state it left it and will refresh the ComboBox data.

Notice the bread crumb trail under the form title of the above grayed out form.  This indicates to the user exactly where they are in the stack of forms.  If they were to click the Item Base hyperlink in the model window, the application would be navigated to the below form.  Notice the new breadcrumb and now the active forms goes from 4 to 5 in the Inventory button.

breadcrumb

If you look the above images, you’ll see several common UI patterns. 

Save and Close buttons and the Add, Edit and Delete buttons.  In the video Billy Hollis spoke to simplifying our UI’s.  I’ve always been a Toolbar developer until now.  I like these buttons and believe end users will too.

Also notice the top of every form is the same.  This is just one of the free features available by using the master page concept form custom control.  See the below section for coverage of this feature.

How Does Non-Linear Navigation Work?

A single ViewManagerService exists for each application and is accessed through the IViewMangagerService Interface.  The ViewManagerService creates and controls one or more ViewManagers.  ViewManagers can only be accessed through the ViewManagerService.  The BBQ Shack has two ViewManagers, one for each Window in the application, ApplicationMainWindow and CashierView.

Accessing the ViewManagerService through the Interface enables IOC scenarios for object construction and makes unit/integration testing of the ViewModels very easy since mock objects can be injected.

Each ViewManager is responsible for keeping track of each form transitioned into and out of view and it also provides the hook for animating forms in and out of view.

The below ApplicaitonMainWindow.xaml has been stripped down to the bare essential to illustrate navigation concepts.

The Grid named applicationLayoutRoot is the Panel where all non-linear navigation will take place.

<Grid>

    <Grid.RowDefinitions...>

    <!-- ViewServiceManger loads/transistions all views here-->
    <Grid Grid.Row="1" x:Name="applicationLayoutRoot" />

</Grid>

The below code fragments are from the above ApplicationMainWindow.xaml code behind file and show how the BBQ Shack navigation is setup and how the application starts up.

The constructor is where the Grid applicationLayoutRoot is registered. The Add method creates a new ViewManager, assigns the Panel to it and provides the ability to hook into the form transitioning process.  This hook also enables platform specific transitions and for developers to create their own.  As you can see below I have provided four transitions for WPF applications with this version of Ocean.

Since the BBQ Shack is a navigation driven application, I have elected to have the Views create most of my ViewModels in the application as you can see in the last line of the constructor.  This is not a rule, it just made sense to me and simplified my application.

Private _objIViewManagerService As IViewManagerService = ViewManagerService.CreateInstance

Public Sub New()
    
    InitializeComponent()    
    
    _objIViewManagerService.Add(Me.applicationLayoutRoot, AddressOf AnimateTransition)
    Me.DataContext = New ApplicationMainViewModel
End Sub

Private Sub AnimateTransition( _
    ByVal objCurrentView As FrameworkElement, ByVal objNewView As FrameworkElement)
    
    'TODO - developers - you can add a user preferences check here to select a different animation 
    '       or no animation
    '
    'Call one of the below built in transitions or write your own.
    '
    'Transistions.CreateInstance.NoTransition(Me, objCurrentView, objNewView)
    'Transistions.CreateInstance.DoublePaneRightToLeftTransition(Me, objCurrentView, objNewView)
    Transistions.CreateInstance.SinglePaneRightToLeftTransition(Me, objCurrentView, objNewView)
    'Transistions.CreateInstance.FadeTransition(Me, objCurrentView, objNewView)
End Sub

Private Sub ApplicationMainWindow_Loaded( _
    ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded    
    
    _objIViewManagerService.Select.Transition(Of HomeView)(My.Settings.HomeView, Nothing, _
                                                            strApplicationSuite:="BBQ Shack")
End Sub

All navigation functions are accessed through the IViewManagerService; this service manages the individual ViewManagers.  The Add method creates new ViewManagers.  The Select method returns a ViewManager.  The ShowWindow method provides a way for an application to launch a new window.  This allows your ViewModel’s to display a new window, either modal or non-modal.  Since the ViewManagerService will be mocked during unit/integration testing, the window will not actually open a new UI.

 iviewmanagerservice

The ViewManager Transition method has the following capabilities:

  • Creates a new instance of a View if required
  • Can pass a primary key to the View; that View’s ViewModel will then look that record up for display
  • Can bring a previously open View into view
  • Can pass a DataContext to a View
  • Automatically handles multiple instances of the same form.  For example, the user could have two separate Person Maintenance forms adding new records and a third form editing a Person record
  • Meets one of my requirements which was to have the navigation work in a single line of code

Transition is the most used method of the ViewManager; it has five possible method signatures that are listed below.

Public Sub Transition(ByVal objNavigateKey As NavigateKey)

Public Sub Transition(Of T As {FrameworkElement, New})(ByVal strViewKey As String)

Public Sub Transition(Of T As {FrameworkElement, New})(ByVal objNavigateKey As NavigateKey, _
                                                       ByVal objDataContext As Object)

Public Sub Transition(Of T As {FrameworkElement, New})( _
            ByVal strViewKey As String, ByVal objDataContext As Object, _
            Optional ByVal objRecordPrimaryKey As Object = Nothing, _
            Optional ByVal strApplicationSuite As String = "")

Public Sub Transition(Of T As {FrameworkElement, New})(ByVal objNavigateKey As NavigateKey)

Before looking at each of the above overloads we need to understand the NavigateKey and its roll.

navigatekey

Each View managed by the ViewManager has a unique NavigateKey associated with it. When a new View is transitioned to a NavigateKey is created if a new one is not passed. 

The ParentNavigateKey is used by the ViewManager to navigate back to the View that opened a new View when the new View is closed.

If the RecordPrimaryKey is assigned, this can be used by the View/ViewModel to retrieve data and display it.

The Version property enables having multiple instance of the same View in the same state.  For example, the user opened the Person Maintenance View and is adding a record.  They then navigate to another form that allows them to open the Person Maintenance View again and add another record.  A customer service person could easily find themselves needing to do this.

When adding a new record the View’s RecordPrimaryKey will be null.  The Version is a simple Integer, that is supplied by the IViewManagerService NextVersion property.  Look back at the above IViewManagerService class diagram to see the NextVersion property.

The ViewKey is an application assigned string value.  I have put all my ViewKeys in the application settings.  You could also put them in a constants file.  Having constants ensures that your application won’t be a victim of a spelling mistake.

Transition Timing Cycle

When the Transition method is called, the following actions occur in the order specified:

  • Check if the requested View has been created, if not it will be created
  • Check if the requested View is the current view, if so stop further process
  • If the requested View is not contained in the registered Panel, add it to the Panel
  • Iterate all Views in the registered Panel and if not collapsed, set Visibility to Collapsed
  • Set the requested View’s Visibility to Visible
  • If the View implements INotifyOnBringIntoView, then call the OnBringIntoView method
  • If the View’s DataContext implements INotifyOnBringIntoView, then call the OnBringIntoView method
  • If the current View implements INotifyOnHidingView, then call the OnHidingView method
  • If the current View’s DataContext implements INotifyOnHidingView, then call the OnHidingView method
  • Set the current View’s ZIndexProperty to 0
  • Set the requested View’s ZIndexProperty to 99
  • Set the requested View’s Opacity to 1
  • Take a snapshot image of the requested View (used by the Task Switcher)
  • Invoke the animation callback that was registered, if no callback was registered then collapse the current View
  • Set the requested View as the current View
  • If the NavigateKey has a primary key value and the DataContext passed in the Transition method implements ILoadable then call the LoadRecord method
  • If the requested View was created and the requested View’s DataContext is implements ILoadable then call the LoadRecord method
  • If the NavigateKey has a primary key value and the View implements ILoadable then call the LoadRecord method
  • If a DataContext was passed in the Transition method, the assign that DataContext to the requested View.

The above is why I like the non-linear navigation API.  It handles so many actions that I would otherwise have to program and simplifies bringing a form into view down to a single line of code.

I also like the ability for Views and ViewModels to sign up for Load, UnLoad and LoadRecord notifications by implementing an Interface and overriding a method instead of responding to events and writing event handlers.

I’m hoping others will be inspired to create even better API’s and black box form management for WPF and Silverlight.  I’ve only got about 4 hours invested in design and coding this API.  It does meet my needs, but I’m sure there are other scenarios I have not taken into consideration.  I welcome all feedback and discussion on this topic.

Let’s have a look at each overload of the Transition method and see how it works and is used in BBQ Shack.

Transition Signature One

Public Sub Transition(ByVal objNavigateKey As NavigateKey)

The above method signature is use to navigate to an existing View, typically from a list of active views.  If the NavigateKey is null or not found an exception is thrown.

_objIViewManagerService.Select.Transition(CType(param, ActiveView).NavigateKey)

The above line of code can be found in 8 locations in the BBQ Shack code where active Views are listed and the user can navigate to them by clicking on their hyperlink.  If you look above to the second image in this post, you’ll see a list of Views, when one is clicked the above code is executed.  The third image shows a list of active item records.  When the Lays chips item is clicked, the above line of code will run.

The ActiveView type is used to encapsulate an active view in lists.  The ActiveView type also provides a DataContext property that UI DataTemplates can consume to display data when the list is rendered.

Transition Signature Two

Public Sub Transition(Of T As {FrameworkElement, New})(ByVal strViewKey As String)

The above method signature is use to navigate to a View that does not perform record maintenance.  Examples of these types of forms are the the HomeView and Cashier MealSelectorView.  There will never be more than one instance of Views loaded using this method signature since there is no Version number or RecordPrimaryKey to differentiate the Views.

_objIViewManagerService.Select.Transition(Of HomeView)(Settings.HomeView)

_objIViewManagerService.Select(STR_CASHIER).Transition(Of MealSelectorView) _
                                (My.Settings.MealSelectorView)

The first line of the above code should now be familiar.

The second line is the same with a twist.  Notice Select(STR_CASHIER).  STR_CASHIER is a constant that is used to tell the ViewManagerService to use the ViewManager identified by the key that has a value returned by the constant STR_CASHIER.

Transition Signature Three

Public Sub Transition(Of T As {FrameworkElement, New})(ByVal objNavigateKey As NavigateKey, _
                                                       ByVal objDataContext As Object)

The above method signature is not used by the BBQ Shack.  However, the other Transition methods call this method internally in the ViewManager, except the first signature that does not need the services provided by this method.  If you study the code in this method signature and the PerformTransition method, you’ll have a full understanding of how the ViewManager provides its services.

Transition Signature Four

Public Sub Transition(Of T As {FrameworkElement, New})( _
            ByVal strViewKey As String, _
            ByVal objDataContext As Object, _
            Optional ByVal objRecordPrimaryKey As Object = Nothing, _
            Optional ByVal strApplicationSuite As String = "")

The above method signature is used twice in the BBQ Shack, both are listed below. 

_objIViewManagerService.Select.Transition(Of HomeView)( _
        Settings.HomeView, Nothing, strApplicationSuite:="BBQ Shack")

_objIViewManagerService.Select.Transition(Of InventoryView)( _
    Settings.InventoryMenuView, Nothing, strApplicationSuite:="Inventory")

It’s primary purpose is to load up a View and assign that View to an ApplicationSuite.

Transition Signature Five

Public Sub Transition(Of T As {FrameworkElement, New})(ByVal objNavigateKey As NavigateKey)

The above method signature is the most commonly found in the BBQ Shack.  This is used when you need open a View and create a new record, or navigate to a View and edit an existing record.

_objIViewManagerService.Select.Transition(Of ItemCategoryView)(_ 
    New NavigateKey(_objIViewManagerService.NextVersion, Settings.ItemCategoryView, _
                    Me.NavigateKey, Settings.Inventory))

The above code demonstrates navigating to a new instance of the ItemCategoryView and setting the Version property to the value returned by the NextVersion property.  This code is run when the Add button is clicked.

Notice the ParentNavigateKey is set to the NavigateKey property of the current ViewModel.

Settings.Inventory sets the application suite this View belongs to.

_objIViewManagerService.Select.Transition(Of UnitOfMeasureView)( _
    New NavigateKey(Settings.UnitOfMeasureView, _
                    CType(param, uom_UnitOfMeasure_FillSelector).uom_UnitOfMeasureIdent, _
                    New NavigateKey(Settings.InventoryMenuView), Settings.Inventory))

The above code demonstrates navigating to the UnitOfMeasureView, instructing the View/ViewModel to load the record with a primary key value equal to value of uom_UnitOfMeasureIdent.  This code is run when a record is selected from the search results listing (see third image above.)

The ViewModel that is calling this code does not have direct access to its Parent’s Parent NavigateKey, so I simply pass a new NavigateKey that will point to the Inventory main View (see third image above).

Settings.Inventory sets the application suite this View belongs to.

Injecting View Manager Services

The below two constructors from the UnitOfMeasureViewModel demonstrate how the BBQ Shack utilizes a default object construction used by the application at run-time, while at the same time exposing the ability to use IOC for object construction or inject mock objects for unit/integration testing.

This ability exists because only Interface types are used.  The first constructor does create concrete objects but those are immediately passed to the second constructor and only the Interface type is used.

Even if you are not using IOC, I would still recommend using this pattern in your MVVM applications to easily enable unit/integration testing.  Additionally, programming to an interface gives you long term flexibility and decoupling that later on you may wish you had.  Cost to do this up front is super cheap as opposed to changing a lot of code later on. 

''' <summary>
''' Default object construction, creates a default BLL and DAL
''' </summary>
Public Sub New()
    Me.New(ViewManagerService.CreateInstance, _
           ViewModelUIService.CreateInstance, _
           BLL(Of uom_UnitOfMeasure).CreateInstance)
End Sub

''' <summary>
''' Used for unit testing or if you want to swap out the default concrete BLL or DAL
''' The DAL is swapped out by passing in a BLL that has a swapped out DAL.
''' </summary>
''' <param name="objIBLL">IBLL(Of uom_UnitOfMeasure)</param>
Public Sub New(ByVal objIViewManagerService As IViewManagerService, _
               ByVal objIViewModelUIService As IViewModelUIService, _
               ByVal objIBLL As IBLL(Of uom_UnitOfMeasure))

    MyBase.New(objIViewModelUIService)
    _objIViewManagerService = objIViewManagerService
    _objIBLLUnitOfMeasure = objIBLL

End Sub

Task Switcher

taskswitcher

I originally released the Task Switch in this blog post and demonstrated it in this application blog post.  One of the readers of my blog asked me to port this code to work with MVVM applications.  When I added the View Manager Services to Ocean this was real easy to accomplish.

The BBQ Shack ApplicationMainWindow listens for the CTRL+TAB key combination and will display the above Task Switcher.  Users can click on the desired task to switch to or they can also keep pressing CTRL+TAB to cycle through active tasks and release the CTRL key to navigate to the selected form.

Try to make application navigation as easy as possible for your users.  They will appreciate your innovation and extra efforts in this space.

The XAML for the Task Switcher is pictured below.  As you can see its stacked on top of the applicationLayoutRoot, centered within Grid.Row 1.

<!-- ViewServiceManger loads/transitions all views here-->
<Grid Grid.Row="1" x:Name="applicationLayoutRoot" />

<local:TaskSwitcherControl 
    Grid.Row="1" x:Name="ucTaskSwitcherControl" HorizontalAlignment="Center" 
    VerticalAlignment="Center" Visibility="Collapsed">
    
    <local:TaskSwitcherControl.BitmapEffect>
        <OuterGlowBitmapEffect GlowSize="8" GlowColor="#FF5A5A5A" />
    </local:TaskSwitcherControl.BitmapEffect>
    
</local:TaskSwitcherControl> 

MVVM Modal and Logging Services – View Model UI Services

Opening a modal dialog window using Window.ShowModal() in a ViewModel will send most seasoned MVVM developers into a rage.  (OK, I might have overstate their reaction, but not by much).

So what’s the MVVM developer to do?  Logically they know they have to put up error dialogs, file open, file save dialogs and folder selection dialogs.  But they also understand that using these dialogs will foul up unit/integration tests or make that task much more complex than it needs to be.

I’ve read several good solutions by fellow developers.  The one I present here works for me, is fully testable and as an added benefit I’ve added logging capabilities to the every dialog.

This service is managed just like the View Manager Services.  The service is passed as an Interface into the constructor of types that need its services.  See the code in the above section, Injecting View Manager Services for an example.

The ViewModelUIService provides a rich set of methods to display message dialogs, file and folder dialogs and to log messages.

The message dialogs are all wrappers around the Task Dialog custom control I wrote back in 2007.  The latest version is in Ocean v2.

 viewmodeluiservice

The Task Dialog custom control mimics the Windows Vista Task Dialog appearance even on Windows XP.  It has the same capabilities and can show the same icons and test regions at the Task Dialog.  Until the .NET Framework provides a wrapper for the Task Dialog, this is a very good option.

 deletedialogbox

Did you notice the Log event in the above class diagram?

If you want to enable the logging of dialogs, in application startup, add a handler for the Log event.  Logging user responses to dialogs in applications is invaluable.  The LogMessage method allows adding a log message without a dialog.

Private Sub Application_Startup(ByVal sender As Object, _
                                ByVal e As System.Windows.StartupEventArgs) Handles Me.Startup
    '
    'TODO - developers - set up logging event handler
    'AddHandler _objIViewModelUIService.Log, AddressOf WriteLogToFile-Database-EventLog-Email-etc
    '
End Sub

For unit/integration testing application startup would not normally run.  Instead, you can add this line of code to your test code to test logging.

The below line of code displays the delete confirmation dialog pictured above.  The (3) is the number of seconds the user will have to wait until they can click the Yes or No buttons.  While waiting, a ProgressBar shows the remaining time.

If _objIViewModelUIService.YesNoConfirmDelete(3) = CustomDialogResult.Yes Then

The below line of code demonstrates showing the end user an exception message.  The idea was to make common programming tasks very easy.  The below ExceptionDialog method creates a dialog with the correct dialog caption, buttons and message.  As a bonus, if the application is a debug build, the Stack Trace is placed in the Additional Text collapsible region of the dialog.

_objIViewModelUIService.ExceptionDialog(e.Error)

Master Page Concept Control

ASP.NET has had master pages since .NET 1.1 (Paul Wilson’s custom control for .NET 1.1. .NET 2.0 added them in the box).

Developers are always looking for ways to simplify the coding of their applications and reduce the need to repeat boiler maker code to a minimum.  Using a master page paradigm also makes it much easier to refactor and change the general appearance of your application without having to actually touch the individual forms.

The below XAML fragment is from BBQShackFormResourceDictionary.xaml and is the BBQShackForm custom control that acts as a master page control.

I wrote about the below AdornerDecorator that wraps the form in this blog post.  Basically, this is required when a form puts up UI Elements in the WPF Adorner layer and that UI is then taken out of view and then brought back into view.  Without this you’ll end up with turds in your adorner layer.

This custom control provides the nice inner and outer borders, form titles, breadcrumb trail and form background. 

<AdornerDecorator>
    <Border x:Name="bdr" Style="{StaticResource formOuterBorderStyle}">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="*" MinHeight="400" />
            </Grid.RowDefinitions>
            <Border Style="{StaticResource formTitleBorderStyle}">
                <StackPanel>
                    <TextBlock Text="{Binding Path=ViewModelFriendlyName}" 
                               Style="{StaticResource formTitleStyle}" />
                    
                    <TextBlock Margin="11,0,0,0" Text="{Binding Path=BreadcrumbTrail}" 
                               Style="{StaticResource formBreadcrumbStyle}" />
                </StackPanel>
            </Border>
            <Rectangle Grid.Row="1" Style="{StaticResource formBackgroundRectangleStyle}" />

            <!--Put the form below here-->
            <ContentPresenter Grid.Row="1" />    
            <!--Put the form above here-->

        </Grid>
    </Border>
</AdornerDecorator>

The below XAML fragment is from UnitOfMeasureView.xaml and is the Unit of Measure maintenance form.

UnitOfMeasureView is a UserControl that derives from EditFormViewBase.  This UserControl’s content is the above BBQShackForm custom control.  The custom control’s content is contained in the below Grid.  See the below image that boxes out the regions on a form.

<Ocean_MVVM:EditFormViewBase ...>
    <local:BBQShackForm>
        <Grid Margin="11">
            <Grid KeyboardNavigation.TabNavigation="Cycle">
                <Grid.RowDefinitions ...>
                <Grid.ColumnDefinitions ...>

                <local:FormStatusUserControl 
                    Grid.ColumnSpan="2" 
                    DateCreated="{Binding Path=uom_UnitOfMeasure.uom_DateCreated}" 
                    DateModified="{Binding Path=uom_UnitOfMeasure.uom_DateModified}" 
                    CreatedBy="{Binding Path=uom_UnitOfMeasure.uom_CreatedBy}" 
                    ModifiedBy="{Binding Path=uom_UnitOfMeasure.uom_ModifiedBy}" 
                    ErrorHeaderText="{Binding Path=ErrorHeaderText}" 
                    WatermarkMessage="{Binding Path=WatermarkMessage}" 
                    IDataErrorInfoError="{Binding Path=uom_UnitOfMeasure.Error}" 
                    ErrorMessage="{Binding Path=ErrorMessage}" 
                    UIValidationErrorMessages="{Binding Path=UIValidationErrorMessages}" />

                <Label Grid.Column="0" Grid.Row="1" Content="Id" />
                <Label Grid.Column="0" Grid.Row="2" Content="Title" />

                <TextBlock Grid.Column="1" Grid.Row="1" 
                           Text="{Binding Path=uom_UnitOfMeasure.uom_UnitOfMeasureIdent}" ... />
                <TextBox x:Name="txtuom_Title" Grid.Column="1" Grid.Row="2" Width="75" 
                         Text="{Binding Path=uom_UnitOfMeasure.uom_Title}” MaxLength="15"/>

                <Grid Grid.Column="0" Grid.Row="3" Grid.ColumnSpan="2" 
                      Grid.IsSharedSizeScope="True" HorizontalAlignment="Right">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition SharedSizeGroup="Buttons" />
                        <ColumnDefinition SharedSizeGroup="Buttons" />
                    </Grid.ColumnDefinitions>
                    <Button Command="{Binding Path=SaveCommand}" Content="Save" ... />
                    <Button Command="{Binding Path=CloseCommand}" Content="Close" ... />
                </Grid>
            </Grid>
        </Grid>
    </local:BBQShackForm>
</Ocean_MVVM:EditFormViewBase>

The FormStatusUserControl provides the FormValidationControl and the two icons on the right side of each form.

I could have added the FormStatusUserControl to the BBQShackForm master page control but chose not to.  At the time I wrote it, I wasn’t sure if I would have boxed myself in, so I kept them separated.  In my next application I may move this to the master page control.

The below image shows a box diagram of the form.  The white box shows the BBQShackForm’s area and how the Grid contents as nested within the BBQShackForm.  Notice the lack of form coloring in the above code.  The BBQShackForm control takes care of all form colors.  This is why I use the master page concept, so that most of my application layout structure and coloring is in one location.  It would be super easy for a developer to change the colors or application layout structure of an application that uses this pattern.

customcontrol

The FormStatusUserControl contains a FormNotification control and two image buttons.

The FormNotification control is similar to the ASP.NET ValidationSummary control.  If the form is valid, the WatermarkMessage is displayed as in the above image, Valid record.  Any errors reported by the Model or ViewModel are listed in the expanded view as pictured below.  The FormNotification can also display other messages like, Record Saved after a successful write to the data store.

This form validation and error reporting is free, and is part of the Ocean framework.  Go back and look at the above XAML.  You’ll notice the FormStatusUserControl has a number of properties that are data bound to the Model and ViewModel.  It is that easy.

 requireditems

The below blue “A” image button opens a form that allows a case correction rule to be added.  The end user scenario is, the user is editing a form and the current case correction rules have made an unwanted correction.  Without interrupting the users workflow or train of thought, they can click the “A”, enter the new rule and go right back to their task.  Try it, you’ll like it.

The below properties icon provides a Tooltip that displays the current record metadata.  I’ve always added the below four fields to all of my database records.  But most users don’t need to see this information on every form, so an inhanced Tooltip made the most sense.

 recordmetadata

Background Loading and Processing of All Data

Windows Forms, WPF and Silverlight UI’s are all single threaded apartments (STA).  The bottom line for you is that you can only access the UI on the same thread that created it.  This means that you can’t spawn an additional thread and have that thread interact directly with the UI.

Side note:  I’ve been thinking about a black box that would make UI programming easier and that would take full advantage of the multiple cores of the host machine without the programmer having to write additional code, manage threads or background workers. 

There has to be a way to get rid of the UI bottleneck so my 8 cores can all spring into action.  I need to go on another cruise!

Microsoft added the Systetem.ComponentModel.BackgroundWorker to the .NET 2.0 framework.  This class encapsulates spawning new threads, optionally getting progress reports and the final results of the work.

BBQ Shack uses the BackgroundWorker for all data access.  By accessing all data off the UI thread, the UI remains response and all animations can run smoothly.  Let me give you an example; in BBQ Shack when you click on a record in the search results, the current form is animated out of view, the new form is animated into view and at the same time the new form is making a call to the database to retrieve the required data.  If this was all accomplished on one thread, BBQ Shack’s performance and usability would be severely degraded.

If you have not programmed with Silverlight, you may not know that all data access must be on a background thread.  If you are planning to write “cloud hosted” applications, you’ll want all your cloud access  performed on a background thread.

If you have not checked out .NET 4.0 Parallel Programming do this today.  This fun and ground breaking computing.

The below code snippet is from the UnitOfMeasureViewModel.vb file.  It shows the minimum code required to use the BackgroundWorker.

LoadRecord is a method on my ViewModel base class.  When this method is called and a primary key of the correct type is passed in, the BackgroundWorker.RunWorkerAsync method is called.  This method will then raise the BackgroundWorker.DoWork event on another thread.  When DoWork has completed its task or an unhandled exception occurs, the BackgroundWorker.RunWorkCompleted event is raised back on the UI thread.  It is that simple, just more code needs to be authored and maintained.  (now you know why I want to create a black box to do this for me)

 Private WithEvents _objBackgroundWorker As New BackgroundWorker

 Public Overrides Sub LoadRecord(ByVal objRecordKey As Object)

     If objRecordKey Is Nothing Then
         Me.uom_UnitOfMeasure = _objIBLLUnitOfMeasure.CreateEntity

     ElseIf Not TypeOf objRecordKey Is Integer Then
         Throw New ArgumentNullException("objRecordKey", AppResources.RecordKeyWrongType)

     Else
         _objLoadKey = objRecordKey
         _objBackgroundWorker.RunWorkerAsync(objRecordKey)
     End If

 End Sub 

Private Sub _objBackgroundWorker_DoWork( _
         ByVal sender As Object, _
         ByVal e As System.ComponentModel.DoWorkEventArgs) _
             Handles _objBackgroundWorker.DoWork

     e.Result = _objIBLLUnitOfMeasure.Select(CType(e.Argument, Integer))
 End Sub

 Private Sub _objBackgroundWorker_RunWorkerCompleted( _
         ByVal sender As Object, _
         ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) _
             Handles _objBackgroundWorker.RunWorkerCompleted

     If e.Error Is Nothing Then

         If e.Result IsNot Nothing Then
             Me.uom_UnitOfMeasure = CType(e.Result, uom_UnitOfMeasure)

         Else
             _objIViewModelUIService.MessageDialog("Data Error", "Record Not Found", ...) 
         End If

     Else
         _objIViewModelUIService.ExceptionDialog(e.Error)
     End If

 End Sub

Notice the very first line of code in the above RunWorkerCompleted method.  You must check the value of e.Error before doing anything else.  Any exceptions from the other thread will be in the Error property.

If no exceptions were thrown on the background worker thread, you can then check the value of e.Result and consume the result.  Remember RunWorkerCompleted is executed back on the UI thread.

Business Entity Validation

I’ve written about business entity validation here and here.  Most of the content in this Code Project article are applicable to Ocean as they have the same API’s.

All of the business entity objects in BBQ Shack are shared by the WPF & Silverlight projects.  All of the validation code is platform agnostic.

One of Ocean’s goals was to enable cross platform business entity objects.  That goal has been achieved.

This works in Silverlight because when I create the Service References, I choose to reused types instead of having the proxy generated code re-implement my types.  I strongly recommend that you never allow the proxy generated code re-implement your types, always reuses your types.

I have trimmed the below code snippet for purposes of illustration.

This c_Customer class is from the c_Customer.gen.vb file.  The .gen. is my naming convention for files that are code generated.  The c_Cusomter type is complete with the c_Customer.vb partial class.  I love that partial classes allow me to add code to types that are code generated.

I’ve used this naming convention in all business applications since my days with SQL Server 7.0.  Essentially it provides a unique name for each object in the database, including columns.  That uniqueness is inherited in my code. 

While it looks strange at first, it makes it SUPER easy to search for all instance of the Customer Email field in my database, including stored procedures, all .NET code, XAML or HTML files.

Behind delivering customer features, code maintenance is my #1 priority.  This naming convention has worked for me accomplish that goal.  You should do that works for you and your team.

<DataContract(Namespace:="Ocean")> _
Partial Public Class c_Customer
    Inherits BusinessEntityBase

    <DataMember(IsRequired:=True)> _
    <CompareValueValidator(ComparisonType.Equal, 0, RequiredEntry.Yes, RuleSet:=STR_INSERT)> _
    <CompareValueValidator(ComparisonType.GreaterThan, 0, _
                           RequiredEntry.Yes, RuleSet:=STR_UPDATE_DELETE)> _
    Public Property c_CustomerIdent() As Integer
        Get
            Return _intc_CustomerIdent
        End Get
        Set(ByVal Value As Integer)
            MyBase.SetPropertyValue("c_CustomerIdent", _intc_CustomerIdent, Value)
        End Set
    End Property

    <DataMember(IsRequired:=True)> _
    <CharacterCasingFormatting(CharacterCasing.ProperName)> _
    <StringLengthValidator(1, 20)> _
    Public Property c_FirstName() As String
        Get
            Return _strc_FirstName
        End Get
        Set(ByVal Value As String)
            MyBase.SetPropertyValue("c_FirstName", _strc_FirstName, Value)
        End Set
    End Property

    <DataMember(IsRequired:=True)> _
    <CharacterCasingFormatting(CharacterCasing.LowerCase)> _
    <RegularExpressionValidator(RegularExpressionPatternType.Email, RequiredEntry.Yes)> _
    Public Property c_Email() As String
        Get
            Return _strc_Email
        End Get
        Set(ByVal Value As String)
            MyBase.SetPropertyValue("c_Email", _strc_Email, Value)
        End Set
    End Property

In BBQ Shack I have chosen to use opt-in data serialization as evidenced by the DataContract and DataMember attributes.

It took me awhile to warm up to the DataContractSerializer as opposed to the default opt-out serialization added to .NET 3.5.  I’m now using the opt-in DataContract serialization for all my WCF applications.

BusinessEntityBase

All my business entity objects derive from BusinessEntityBase.  This class provides the plumbing necessary for deriving objects to have  property and object validation,  case correction of entity values, change notification, validation interfaces implementation and other features.

The MyBase.SetPropertyValue method provides the entry point to have data validated, case correction applied and change notification.

Applying Validation Rules

Validation rules can be applied as attributes to properties, in code as shared rules or in code as an instance rule.  I like using the attributes because they are super easy to code generate, the rule is physically near the property so that when I read a business entity’s code I can “see” everything related to this property.

There are .NET restrictions as to data an attribute can contain.  For example you can put a function in an attribute so creating a rule that states that the date entered must be before Date.Now() is not permitted.  This is where the ability to add a rule in code comes into play.

For a great example of this open the BBQ Shack file, ccc_CharacterCasingCheck.vb located in the BBQShack.BusinessEntityObject project.  Now expand the Methods region.  In the AddSharedValidationRules method, not only did I add a business rule, but pointed that rule to some code I wrote that is specific to this class and will never be used again.

Ocean provides a rich set of lines of business validation rules you can use.  However, if you need your own, writing them is very easy.

  • BankRoutingNumberRule
  • ComparePropertyRule
  • CompareValueRule
  • CreditCardNumberRule
  • DomainRule
  • InRangeRule
  • NotNullRule
  • RegularExpressionRule
  • StateAbbreviationRule
  • StringLengthRule

I have also designed the validation attributes to accept both Decimal and Date values in the attribute.  Normally you can’t do this because values of these types can’t be used in an attribute.  For one example of this look at the Sale_Item.vb file in the BBQShack.BusinessEntityObjects project.

Shared validation rules are rules that all instances of a type will use.  The first time a type is created, all shared rules are added to a dictionary for the type so you only pay the cost to parse the attributes once.

Instance validation rules are added each time an instance of an object is created.  There is an AddInstanceValidationRules method in BusinessEntityBase that deriving types can override and provide required implementation.

I strongly recommend that you look at every business entity class.  You’ll find many real world examples of business rules being applied.

Rule Set

In the above code, the first validation attribute has the RuleSet property set to STR_INSERT.  This rule will only be applied when the business object is in insert mode; the mode of the object is set by assigning the BusinessEntityBase.RuleSet property.

Apply Case Correction Rules

In the above code you’ll also notice the CharacterCasingFormatting attribute in use.  This attribute adds a shared rule that gets applied by the BusinessEntityBase when a property value is set.  The following rules are provided by Ocean.

  • None
  • LowerCase
  • OutlookPhoneNoProperName
  • OutlookPhoneProperName
  • OutlookPhoneUpper
  • ProperName
  • UpperCase

The case correction API also provides hooks for developers to inject their own application specific case correction rules.  BBQ Shack provides a UI for end users to do just that.

IBLL(Of T)  c# – IBLL<T>

Superstar Glenn Block and I were in one of our code jam sessions and the idea to get rid of boiler maker business logic layer and data layer code came up.  I had originally written BBQ Shack with concrete BLL and DAL types for each business object.  That is one standard way to code these layers that many developers use.  I like it because its very easy to code generate and extend later as required.

Glenn and I cranked up a new Unit Test project, using TDD wrote IBLL(Of T) and IDAL(Of T).  I spend a few evenings of refactoring, creating concrete types and changed BBQ Shack over to use these.  Bottom line was I deleted to directories of files.  Part of the refactoring was injecting the concrete implementation of IBLL(Of T) into my ViewModels.  The testing of my ViewModels can be against a database or with mock objects.  Like the View Manager Services the ViewModels can be created by IOC too.

The main purpose of IBLL(Of T) was to relieve the developer from coding the most common business and data layer functions that deal with data store operations (CRUD).  These would be consumed by other developer written business or data layer code.

Real World Example:  When a new Customer is added, the business layer must initiate adding 3 other records and updating 2 more.  The ViewModel would call a single method in a developer written business layer method.  That method would utilize the IBLL(Of T) methods when reading and writing to the data store.  In this example, the developer only had to code the “real” business logic and not boiler maker code.

Just a quick glance at the below Interface will show the basic methods most BLL’s have today.  This is one of the four Interfaces that make up the generic BLL API.

Public Interface IBLL(Of T As {IBusinessEntity, Class, New})

  Function CreateEntity() As T

  Function Delete(ByVal intKey As Integer, ByVal objTimeStamp As Byte(), _
                    Optional ByVal strProcedureName As String = "") As Integer

  Function Delete(ByVal strKey As String, ByVal objTimeStamp As Byte(), _
                    Optional ByVal strProcedureName As String = "") As Integer

  Function Delete(Of K)(ByVal Key As K, ByVal objTimeStamp As Byte(), _
                          Optional ByVal strParameterName As String = "", _
                          Optional ByVal strProcedureName As String = "") As Integer

  Function Insert(ByVal Entity As T, ByRef intNewIdent As Integer, _
                    Optional ByVal strProcedureName As String = "") As Integer

  Function Insert(ByVal Entity As T, Optional ByVal strProcedureName As String = "") As Integer

  Function [Select](ByVal intKey As Integer, Optional ByVal strProcedureName As String = "") As T

  Function [Select](ByVal strKey As String, Optional ByVal strProcedureName As String = "") As T

  Function [Select](Of K)(ByVal Key As K, Optional ByVal strParameterName As String = "", _
                            Optional ByVal strProcedureName As String = "") As T

  Function Update(ByVal Entity As T, Optional ByVal strProcedureName As String = "") As Integer

End Interface

The BLL(Of T) and DAL(Of T) by default use a naming convention to call the correct stored procedure for database operations and to property name the parameters in the stored procedure calls.  In the case where a specific or unconventional name is required, the Optional ProcedureName parameter can be supplied.

Let’s take a look at the IBLL(Of T).Update method being called in a ViewModel.

Notice that the business object IsValid method is being called before Update is called.  If the IsValid method returns false, the ErrorHeaderText property is set to Save Errors. 

I always call the IsValid method in every layer of my code.  Doing this in the UI, ViewModel, BLL and DAL ensures that an invalid object never gets consumed.  Individual layers should be programmed defensively and never trust other layers (developers; LOL). 

Private Function Update() As Integer

    Try
        Me.uom_UnitOfMeasure.uom_DateModified = Now
        Me.uom_UnitOfMeasure.uom_ModifiedBy = Data.UserName

        If Me.uom_UnitOfMeasure.IsValid Then

            If _objIBLLUnitOfMeasure.Update(Me.uom_UnitOfMeasure) = 1 Then
                Return 1
            End If

        End If

        Me.ErrorHeaderText = "Save Errors"

    Catch ex As Exception
        _objIViewModelUIService.ExceptionDialog(ex)
    End Try

    Return 0
End Function

If you are new to WPF the purpose of setting the above ErrorHeaderText property or how it works may not be obvious. 

If you look back to the above XAML for the UnitOfMeasureView UserControl, you’ll notice that the ErrorHeaderText property on the FormNotification control is data bound to the DataContext’s ErrorHeaderText property.  This property has built-in change notification, so that when the value is assigned in a ViewModel, that value is automatically consumed by the UI.

This is one of programming paradigm differences between WPF and other UI technologies.  In WPF or Silverlight we set properties in data that is bound to the UI.  In other UI technologies, we would normally set a property on a control in the UI.  The WPF pattern is much easier to program and when you adopt MVVM is much easier to unit/integration test.

The actual call to the business layer is done through the Interface and is a single line of code, wrapped in a Try Catch block.  Any errors are reported to the end user and are logged by the method call to the View Model UI Service in the Catch block.

Ocean provides concrete implementations for each of the business and data layer Interfaces that make up the IBLL(Of T) and IDAL(Of T) story.  These concrete implementations are coded the way I would program my layers.  You can easily rewrite them to comply with how you do things, or you can just inject your own concrete implementations of the Interfaces.  You are not locked into doing things the way I do, not at all.

Below is the default concrete implementation of the BLL(Of T) provided by Ocean.  DAL is a private property of type IDAL(Of T).

Public Function Update( _
    ByVal Entity As T, _
    Optional ByVal strProcedureName As String = "") As Integer Implements IBLL(Of T).Update

    Return DAL.Update(Entity, strProcedureName)
End Function

There are many very solid and wildly accepted reasons for layering your applications. One is that it enables the swapping out of data layers without having to change your application Views or ViewModels.

If I wanted to move my data store from a local SQL Server database to Azure or to SQL Server Compact or to a WCF Service, I would only have to provide a different concrete implementation of IDAL(Of T) and recompile. 

Heck, you could even just make that a config setting, have your IOC pick it up and with the flick of a user setting, change the data store.  Now you really understand why programming asynchronous data access up front is a must; it provides your applications the flexibility they need to meet the ever changing technologies we work with.

IDAL(Of T)   c#- IDAL<T>

IDAL(Of T) is the lower application layer that IBLL(Of T) consumes to perform data store operations.  The UI or ViewModels would never call methods on IDAL(Of T) directly.

IDAL(Of T) has the same Interface methods as IBLL(Of T) so I won’t repeat that code.  There are a total of four matching Interfaces exposed by IDAL that are consumed by IBLL.

The concrete implementation of IDAL(Of T) provided by Ocean is programmed against SQL Server 2008.  To target other data stores all that is required is a new concrete implementation of IDAL(Of T).

The hardest piece of the puzzle to solve was the creation of the stored procedure calls.  Proper ADO.NET parameters must be created, in the case of BBQ Shack and the provided concrete DAL implementation I had to create SQLParameters.  This requires knowledge of the business objects data store characteristics.

In BBQ Shack, I used an attribute on the business object properties to provide the required metadata.  These were easy to code generate and the action of parsing the attribute only has to be done once.  There you go, that is the trade off.  Add a code generated attribute and I get to delete two directories of files that I’ll never have to maintain going forward. 

The below code snippet is from the uom_UnitOfMeasure.vb file in the BBQShack.BusinessEntityObjects project.

The DataColumn attribute provides all the required metadata for the concrete implementation of DAL(Of T) to create SQLParameters or other types of parameters.  DataColumn has other properties and overloads that are not used below.

With a quick glace at the below code snippet and you can easily determine the data store characteristics of each property. 

Did you notice the lack of Booleans being used in any of my attributes.  Spending a few extra minutes to create an Enum pays off big time with respect to code readability.

<DataColumn(ColumnType.Timestamp)> _
<NotNullValidator(RuleSet:=STR_UPDATE_DELETE)> _
Public Property uom_Timestamp() As Byte()
    Get
        Return _objuom_Timestamp
    End Get
    Set(ByVal Value As Byte())
        MyBase.SetPropertyValue("uom_Timestamp", _objuom_Timestamp, Value)
    End Set
End Property

<DataColumn(ColumnType.VarChar, 15)> _
<CharacterCasingFormatting(CharacterCasing.LowerCase)> _
<StringLengthValidator(1, 15)> _
Public Property uom_Title() As String
    Get
        Return _struom_Title
    End Get
    Set(ByVal Value As String)
        MyBase.SetPropertyValue("uom_Title", _struom_Title, Value)
    End Set
End Property

<DataColumn(ColumnType.Int, PrimaryKey.Yes, Identity.Yes)> _
<CompareValueValidator(ComparisonType.Equal, 0, _
                       RequiredEntry.Yes, RuleSet:=STR_INSERT)> _
<CompareValueValidator(ComparisonType.GreaterThan, 0, _
                       RequiredEntry.Yes, RuleSet:=STR_UPDATE_DELETE)> _
Public Property uom_UnitOfMeasureIdent() As Integer
    Get
        Return _intuom_UnitOfMeasureIdent
    End Get
    Set(ByVal Value As Integer)
        MyBase.SetPropertyValue("uom_UnitOfMeasureIdent", _
                                _intuom_UnitOfMeasureIdent, Value)
    End Set
End Property

Ocean provides its own data stack contained in the DataAccess class.  DataAccess provides many services like connection string management, connection management, functions with overloads for accessing ADO.NET data methods, concurrency violation notification and index violation notification.

The below concrete implementation of the Update method is from the DAL.vb file in the OceanFrameworkGenericLayers project.

It first performs standard sanity checks, then builds the parameters and finally calls the ExecuteNonQuery method in Ocean’s DataAccess class.

I had thought about caching the created parameters and then just update them, but in the end, this was easier and performance was not affected.

Public Function Update( _
    ByVal obj As T, Optional ByVal strProcedureName As String = "") _
        As Integer Implements IDAL(Of T).Update

    If obj Is Nothing Then
        Throw New ArgumentNullException("obj", "Business object was null")
    End If

    If obj.ActiveRuleSet <> STR_UPDATE Then
        Throw New InvalidOperationException( _
            String.Format("{0} using wrong ActiveRuleSet for update.  Current setting: {1}.", _
                      Me.EntityName, obj.ActiveRuleSet))
    End If

    If Not obj.HasBeenValidated Then
        Throw New InvalidOperationException( _
            String.Format("{0} has not been validated for update.", _
                    Me, EntityName))
    End If

    Dim da As DataAccess = GetDataAccess()
    Dim params As New List(Of SqlParameter)

    For Each kvp As KeyValuePair(Of String, DataColumn) In _
            SharedDataColumnsManager.GetManager(Me.EntityType).DataColumnDictionary.ToList

        If kvp.Value.IsDateCreated OrElse kvp.Value.IsCreatedBy Then
            Continue For
        End If

        If kvp.Value.Nullable Then

            Dim objValue As Object = obj.GetType.GetProperty(kvp.Key).GetValue(obj, Nothing)

            If objValue Is Nothing Then
                params.Add(BuildSQLParameter( _
                           kvp.Value.ParameterName, _
                           GetSqlDataType(kvp.Value.ColumnType), _
                           ParameterDirection.Input, DBNull.Value))

            Else
                params.Add(BuildSQLParameter( _
                           kvp.Value.ParameterName, _
                           GetSqlDataType(kvp.Value.ColumnType), _
                           ParameterDirection.Input, objValue))
            End If

        ElseIf kvp.Value.MaximumLength > 0 Then
            params.Add(BuildSQLParameter( _
                       kvp.Value.ParameterName, _
                       GetSqlDataType(kvp.Value.ColumnType), _
                       kvp.Value.MaximumLength, ParameterDirection.Input, _
                       obj.GetType.GetProperty(kvp.Key).GetValue(obj, Nothing)))

        Else
            params.Add(BuildSQLParameter( _
                       kvp.Value.ParameterName, _
                       GetSqlDataType(kvp.Value.ColumnType), _
                       ParameterDirection.Input, _
                       obj.GetType.GetProperty(kvp.Key).GetValue(obj, Nothing)))
        End If

    Next

    Return da.ExecuteNonQuery( _
            CommandType.StoredProcedure, _
            GetStoredProcedureName(Me.UpdatestrProcedureName, strProcedureName), _
            params.ToArray)

End Function

I  have not used IBLL(Of T) or IDAL(Of T) in a production application.  BBQ Shack is the first application. 

What I like about it is that all my CRUD business and data layer implementation is its basically in two files instead of boiler maker code repeated in many files.

I know, jury is still out, but I wanted to share the concept with you.

Silverlight Code Sharing

If you look at the BBQShack.BusinessEntityObjects.Silverlight project you’ll see that all the files are linked to files in the BBQShack.BusinessEntityObjects project.

Now for the real benefit of code sharing with Silverlight.

Look at the Service Reference in the BBQShack.Silverlight project pictured below.  Notice that I provided specific hints for type reuse. 

By doing this, the proxy wizard did not re-implement my business objects and I can now take full advantage of all the code in my business objects.

servicereference

I have written about Silverlight code sharing in this blog post.

System Requirements

  • Visual Studio 2008 with SP1  (I have not tried BBQ Shack on Visual Studio 2010)
  • Silverlight 3 Developer Version (not Silverlight 3 Beta, not Silverlight 4 Beta)
  • Note:  If you cannot create a Silverlight application in Visual Studio, you do not have the Silverlight Developer version installed.
  • SQL Server 2008 or SQL Server 2008 Express (select install from the 3rd or 4th column)

Downloads

Sky Drive BBQ Shack Source Code, Database and Ocean v2  (opens page where three zip files are located.)

You must download all three of the zip files.  The BBQShack and Ocean zip files should be unziped in the same folder, like your \Projects folder.  After unzipping the folders will look like this:

…\Projects\BBQShack

…\Projects\Ocean

Doing this will ensure that the Ocean projects can be found by the BBQ Shack solution.

After unzipping the above download zip files, you will have these folders in your projects folder.

folders

Database Installation

The above BBQ Shack download includes the SQL Server database .mdf and .ldf files.

If you do not have SQL Server Express 2008 download it from this page.  Select the install from the 3rd or 4th column of the download page.

If you have a SQL Server 2008 you must “attach” the database to an instance of SQL Server 2008.

If you have SQL Server Express you either can either “attach” the database to an instance of SQL Server Express or you can use the file attachment method in your connection string.

Next you must change the connection string in the BBQShack project and the BBQShack.Silverlight.Web project. 

To do this double click on the “My Properties” icon below the project name in the Solution Explorer.  Select the Settings tab and edit the connection string “Data Source” property of the connection string.  You MUST do do this for BOTH projects.

Close

BBQ Shack was a fun adventure that took me places (including Alaska) I never thought I would go.  I hope that you can learn from the code and that some of the innovation in this project provides inspiration and possibly adoption.

Have a great day,

Just a grain of sand on the worlds beaches.


WPF & Silverlight Designer Team – New Blog

January 18, 2010

BlueCiderLogo

The WPF & Silverlight Designer Team (Cider Team) has just launched a new team blog at: http://blogs.msdn.com/wpfsldesigner/default.aspx

During the months of January and February 2010 we will be adding a good bit of WPF & Silverlight content. 

This initial content will focus on the Designer toolset and walkthroughs using the Designer as well as some posts for control authors.

You can leave feedback for the Cider Team on the blog or you can send comments and questions using Twitter and include #wpfdesigner or #sldesigner in your tweets.

Have a great day,

Just a grain of sand on the worlds beaches.


Extensibility Series – WPF & Silverlight Design-Time Code Sharing – Part II

January 11, 2010

In my previous post WPF & Silverlight Design-Time Code Sharing – Part I, I introduced our custom controls and the required design-time features.  I also covered how Visual Studio 2010 discovers and loads control design-time assemblies.  In addition, I explained how to implement platform neutral WPF & Silverlight type resolution in a common design-time assembly.

In this post I’ll tie the design-time metadata to each of the design-time features.  This post will be more on how you can implement features in your design-times rather than a detailed analysis of the sample solution  since the the sample solution code has detailed comments and a walk-through the code using the Visual Studio Task List as the navigator for the code. 

I have provided many external links in this post and suggest that you read them to ensure you have the required understanding of design-time extensibility for WPF & Silverlight Designer.

I have updated the code download for the WPF & Silverlight Design-Time Code Sharing – Part I.  If you previously downloaded the code, please re-download the updated code from this post or Part I.  I made a change to the FeedbackControlInitializer.cs that I’ll point out in the Control Default Initializer section below along with a few very minor changes to comments.

To follow along, open the sample solution and view the CiderControlsAttributeTableBuilder.cs file in the CiderControls.Common.VisualStudio.Design project.

Metadata Loading Revisited

If you read the referenced MSDN pages in the example code project, you will notice that some MSDN examples load metadata by applying a design-time attribute to the run-time control.  In other words, the design-time metadata is in the control assembly and not in a separate design-time assembly.  These MSDN examples were written to show you that you “can” load metadata this way, but this is not considered a best practice.

For example, view the DefaultInitializer Class MSDN page.  You will notice in article’s example code that the default initializer metadata has been added to the Button’s class declaration using the Feature attribute.

It’s recommended that control developers place their design-time metadata in a design-time assembly and not in the control assembly.  Having design-time metadata prevents your run-time controls from having a reference to Microsoft design assemblies.  Additionally if you do not have separate design-time assemblies, you by-passes the built-in metadata loading feature of allowing Visual Studio and Blend to have common as well as separate code.  Another advantage of having your design-time code in design assemblies is that you can ship new design assemblies without having to ship a new version of your control assembly.

Please read the Metadata Store MSDN topic for a full discussion of metadata.

Control Default Initializer

Control default initializers are the proper way to assign initial design-time values to properties when a control is created on the design surface using the ToolBox.

The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata for the Feedback control’s control initializer.

new FeatureAttribute(typeof(FeedbackControlInitializer))

Control default initializers must derive from DefaultInitializer and override the InitializeDefaults method as in the below example.

using CiderControls.Common.VisualStudio.Design.Infrastructure;
using Microsoft.Windows.Design.Model;

namespace CiderControls.Common.VisualStudio.Design.Controls {

  //TODO 14 - FeedbackControlInitializer

  /// <summary>
  /// An initializer is called during control creation on the design surface
  /// You can set properties on the ModelItem to adjust the default values.
  /// </summary>
  internal class FeedbackControlInitializer : DefaultInitializer {

    public FeedbackControlInitializer() {
    }

    /// <summary>
    /// Callback when the designer has created the FeedbackControl.
    /// </summary>
    public override void InitializeDefaults(ModelItem item) {
      base.InitializeDefaults(item);
      // The below values are set demonstration purposes to show how its done.
      // See how nice the platform neutral PropertyIdentifiers work.  
      // ex: MyPlatformTypes.Feedback.CornerRadiusProperty
      item.Properties[MyPlatformTypes.Feedback.CornerRadiusProperty].SetValue("10");
      item.Properties[MyPlatformTypes.Feedback.BorderThicknessProperty].SetValue("2");
      item.Properties[MyPlatformTypes.Feedback.BorderBrushProperty].SetValue("LightGray");
    }
  }
}

You can access a property in the ModelItem.Properties collection by using either the name of the property or by using property identifier as I have done above.  Using a property identifier removes quoted strings from your code and IntelliSense provides easy access to defined property identifiers.

The SetValue method takes a parameter of type object and converts values as required.  Notice how “10” is converted to a CornerRadius, “2” is converted to a Thickness and “LightGray” is converted to a SolidColorBrush object.

ModelItem SetValue Method

In the original code download I had a mistake by trying to set the above CornerRadius value using the syntax .SetValue(10) instead of the correct syntax .SetValue(“10”).  Visual Studio 2010 Beta2 swallowed the exception.  All future versions of Visual Studio 2010 will report this as an exception, which is the correct behavior.  I have updated the code download with the corrected code.

SetValue takes an object as the method parameter.  If a string is passed, a type converter will be used to assign the value as in the above code.  Type converters are also used to convert string values in XAML files to property values.

If a string is not passed, an object that matches the type of the property must be passed.

Example, property type is Integer, using .SetValue(10) or .SetValue(“10”) is correct.

Example, property type is Thickness, using .SetValue(10) is not correct because 10 is not a Thickness.  The correct way to pass this parameter is to use (“10”) which will be converted to a Thickness.  You can also pass in a platform specific Thickness object instead of (“10”).

Control Context Menu

MenuAction

A design-time context menu can be added to controls on the design surface.  You can use context menus for many purposes such as setting control values or opening a dialog.

The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata for the Feedback control’s context menu.

new FeatureAttribute(typeof(FeedbackControlContextMenuProvider))

To implement a context menu provider, your class must derive from ContextMenuProvider or a class that derives from ContextMenuProvider.  In the example solution I have derived my ContextMenuProvider from PrimarySelectionContextMenuProvider.  PrimarySelectionContextMenuProvider adds additional functionality to ContextMenuProvider by automatically displaying the context menu when the control is selected on the design surface when the developer right clicks the control.

Implementing a context menu is very simple.  In the constructor add a MenuAction for each desired context menu item to your class’s Items collection.  You can add one or more MenuActions and MenuActions can be nested.  MenuActions can also be marked as Checkable as in the above image.

The UpdateItemStatus event is raised just before the context menu item is displayed.  At your option you can hide or display items, enable or disable items and if the menu item is Checkable, set it as Checked or not.  The sample solution uses UpdateItemStatus to Check the MenuAction that represents the current value of the Feedback control.

When adding the MenuAction you must also provide implementation for the MenuAction Execute event.  The Execute event is raised when the menu item is clicked.

The below FeedbackControlContextMenuProvider constructor demonstrates setting up the handler for UpdateItemStatus event, the creation of a sub menu and adding items to it.

public FeedbackControlContextMenuProvider() {

  this.UpdateItemStatus +=
    new EventHandler<MenuActionEventArgs>
      (FeedbackControlContextMenuProvider_UpdateItemStatus);

  //MenuGroup enables having submenus
  _feedbackGroup = new MenuGroup(Constants.STR_FEEDBACKGROUP, Strings.MenuFeedbackSetValue);
  _feedbackGroup.HasDropDown = true;

  // Add the 6 MenuActions
  for (int i = Constants.INT_MINIMUMRATINGVALUE; i < Constants.INT_MAXIMUMRATINGVALUE + 1; i++) {
    FeedbackMenuAction menuItem =
        new FeedbackMenuAction(
            Strings.ResourceManager.GetString(string.Format("MenuFeedbackSetValue{0}", i)), i);
    menuItem.Checkable = true;
    _feedbackGroup.Items.Add(menuItem);
    menuItem.Execute += new EventHandler<MenuActionEventArgs>(FeedbackSetValueMenuAction_Execute);
  }

  this.Items.Add(_feedbackGroup);
}

When the HasDropDown property is true, it will display its sub items in a fly out menu.  If false, the sub items will be listed inline in the context menu.

To make a context menu item (MenuAction) Checkable, set the Checkable property to true.  To check a menu item, set the Checked property to true.

Set up an event handler for each menu item’s Execute event.  This code will be called when the menu item is clicked at design-time.

Control Adorner

Design-time control adorners can be used to provide additional UIElements on the design surface adding features like selection handles, grid lines, grid rails, buttons or other design-time features that your control requires at design-time.  The display of an adorner is controlled by a SelectionPolicy.  I recommend that you read up on the base class that all policies like SelectionPolicy derive from, ItemPolicy.

MSDN has a great two adorner articles Adorner Architecture and AdornerProvider Class that provide a solid understanding of adorners.  These articles also have several links to example code for implementing an adorner.

In the below image, the Feedback control design-time exposes a Rating Control in an adorner to provide a design-time interactive way to set the value of the Feedback control.  The lower set of 3 green ellipses and 2 black ellipses is the adorner.  

Adorner

The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata for the Feedback control’s adorner.

new FeatureAttribute(typeof(FeedbackControlAdornerProvider))

To implement an adorner your class must derive from AdornerProvider or a class that derives from AdornerProvider like PrimarySelectionAdornerProvider. 

The adorner in the sample solution derives from AdornderProvider.  Using this class instead PrimarySelectionAdornerProvider requires that we do more work to control when our adorner is visible, but this also gives us more control over when the adorner is displayed. 

PrimarySelectionAdornerProvider will display the adorner when the control is selected on the design surface.  This would be desirable for adorners like resize, control alignment, grid rails or control size label adorners.

However, our adorner is only used to set a control value so we didn’t want this adorner showing when the Feedback control is being resized or moved.  This was accomplished by deriving from AdornderProvider and applying a SelectionPolicy to the adorner class.

The below FeedbackControlAdornerProvider code is an example shell for an adorner.  I have stripped out the implementation code for clarity so we can focus on writing an adorner.  The solution sample code has comments for how I implemented the adorner in the sample.

The UsesItemPolicy attribute is used to associate a policy with an adorner.   The policy determines when the adorner is visible.

The IsToolSupported method allows you to inform the Designer if your adorner supports the currently selected tool or not.  Your adorner will not be displayed if the adorner does not support the current tool.  The Visual Studio 2010 WPF & Silverlight Designer ships with two tools, CreationTool and the SelectionTool.  The below IsToolSupported code demonstrates how to indicate that the adorner does not support the CreationTool.  

The Activate method can be used to create your adorner and add required event handlers.  At your option, you can elect to create your adorner each time Activate is called or you can create the adorner once here or in the adorner constructor and maintain a module level reference to it.

The Deactivate method is used to unhook any event handlers added in the Activate method.

Please beware that Activate and Deactivate may be called several times during the adorner lifetime.

[UsesItemPolicy(typeof(FeedbackControlSelectionPolicy))]
internal class FeedbackControlAdornerProvider : AdornerProvider {

  /// <summary>
  /// The main kinds of tools are:
  ///   - CreationTool (when the toolbox is activated - and clicking means create new object)
  ///   - SelectionTool (when clicking means select object)
  ///   
  /// For this adorner, we don't want it to be in the way of creating new objects 
  /// so we'll hide it when the current tool is not the SelectionTool. 
  /// </summary>
  /// <param name="tool"></param>
  /// <returns></returns>
  public override bool IsToolSupported(Tool tool) {
    if (tool is SelectionTool) {
      return true;
    }
    return false;
  }

  /// <summary>
  /// Activate is called when the policy (specified by the UsesItemPolicy attribute)
  /// for the adorner provider says it's found a new model item.  
  /// </summary>
  protected override void Activate(ModelItem item) {

    // Create the adorner
    // Add required event handlers

    base.Activate(item);
  }

  /// <summary>
  /// Deactivate is called when the policy says a previously active item is no 
  /// longer within the policy. 
  /// </summary>
  protected override void Deactivate() {

    // unhook event handlers added in the Active method.
    base.Deactivate();
  }
}
Controlling Adorner Display

The below FeedbackControlSelectionPolicy class is a generic implementation of a selection policy that displays the adorner when the control is selected, but hides it when the control is being moved or resized.  You can reuse this selection policy in your applications to get the same behavior.

The below code is commented in detail.

using Microsoft.Windows.Design.Interaction;
using Microsoft.Windows.Design.Model;
using Microsoft.Windows.Design.Policies;

namespace CiderControls.Common.VisualStudio.Design.Controls {

  //TODO 11 - FeedbackControlSelectionPolicy

  /// <summary>
  /// This FeedbackControlSelectionPolicy provides for hiding of the adorner when the
  /// Feedback control is being resized or moved. 
  /// </summary>
  internal class FeedbackControlSelectionPolicy : PrimarySelectionPolicy {

    private bool _isFocused;

    public FeedbackControlSelectionPolicy() {
    }

    /// <summary>
    /// Determines if an item is part of the policy.  Called back from 
    /// OnPolicyItemsChanged on all the items in e.ItemsAdded 
    /// </summary>
    /// <param name="selection"></param>
    /// <param name="item"></param>
    /// <returns></returns>
    protected override bool IsInPolicy(Selection selection, ModelItem item) {
      bool inPolicy = base.IsInPolicy(selection, item);
      return inPolicy && !_isFocused;
    }

    /// <summary>
    /// Called when this policy is activated.  In order to determine 
    /// which items are in/out of a policy, we need to subscribe to events.
    /// The base class, PrimarySelectionPolicy, subscribes to 
    /// changes in selection.  Additionally we need to subscribe to changes
    /// in the focused task so we can disable during resize/move.
    /// </summary>
    protected override void OnActivated() {
      this.Context.Items.Subscribe<FocusedTask>(OnFocusedTaskChanged);
      base.OnActivated();
    }

    /// <summary>
    /// When the policy is no longer in use, we unsubscribe from the events we hooked up to 
    /// in Activated.
    /// </summary>        
    protected override void OnDeactivated() {
      this.Context.Items.Unsubscribe<FocusedTask>(OnFocusedTaskChanged);
      base.OnDeactivated();
    }

    // If the Feedback control is selected and there is a FocusedTask like moving or 
    //   resizing the control, the Feedback control will be added to the Removed 
    //   parameter and the adorner will be removed from the Feeback control.
    // During the drag or resize operation this code will be bypassed on repeated calls until
    //   the move or resize is completed.

    // If the Feedback control is selected and there is no FocusedTask like moving or being 
    //   resized, the Feedback control will be added to the Added parameter and the adorner
    //   will be shown on the Feedback control.

    /// <summary>
    /// Tasks are operations on tools, for example, the drag resize task is an operation
    /// on the SelectionTool, and the click draw task is an operation on the CreationTool.
    /// 
    /// When a focused task is active we want to hide the ratings control because
    /// we don't know what the interaction will be.
    /// </summary>
    /// <param name="f"></param>
    private void OnFocusedTaskChanged(FocusedTask f) {
      bool nowFocused = f.Task != null;
      if (nowFocused != _isFocused) {
        _isFocused = nowFocused;

        Selection selection = Context.Items.GetValue<Selection>();
        if (selection.PrimarySelection != null) {
          ModelItem[] removed;
          ModelItem[] added;

          if (nowFocused) {
            removed = new ModelItem[] { selection.PrimarySelection };
            added = new ModelItem[0];
          } else {
            removed = new ModelItem[0];
            added = new ModelItem[] { selection.PrimarySelection };
          }

          OnPolicyItemsChanged(new PolicyItemsChangedEventArgs(this, added, removed));
        }
      }
    }
  }
}

Category Editor

Category editors are used in the properties window category view to provide a custom UI for editing related properties in a specific category.  The Text category editor is a good example of a category editor.  Category editors are implemented as DataTemplates.  DataTemplates provide developers the freedom  to implement any UI for editing properties.

I strongly recommend that you read the MSDN topics Property Editing Architecture and Property Editing Namespace.  These will provide you an outstanding overview of property editing.

CategoryView

The Feedback control has a Custom category editor that is pictured above.  This editor allows the four custom properties exposed by the Feedback control to be edited as a group.

You can include the PropertyMarker in your category editors as I have done above if you desire.  The PropertyMarker provides a lot of free functionality such as applying a data binding, applying a resource, extracting a value to a resource or resetting the property value.

Properties can be edited using the default PropertyValueEditor or a custom PropertyValueEditor.  Notice the above Value property has a custom PropertyValueEditor.  See the Property Value Editor section below for registering and implementing a PropertyValueEditor.

Examples of a default PropertyValueEditor are TextBoxes for strings or numbers, CheckBoxes for Booleans and ComboBoxes for properties of an enum type.  If you do not assign a custom PropertyValueEditor the Designer will select the most appropriate PropertyValueEditor for you without any action on your part.  In the above category editor, the first three properties are using the default PropertyValueEditor.

A custom PropertyValueEditor is used to provide a custom UI for setting a value on the property.  The above category editor Value property is edited using the Rating control.

The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata for the Feedback control’s category editor.

AddCategoryEditor(feebackType, typeof(FeedbackControlCategoryEditor));

To implement a category editor your class must derive from CategoryEditor.

When the ConsumesProperty method returns true, this indicates that the property is included in the category editor and won’t be listed outside of the category editor.  If ConsumesProperty returns false, the property will be listed as a separate row within the category but outside the category editor.  This category editor provides editing for all properties in the Custom category so ConsumesProperty always returns true.

The EditorTemplate property returns a DataTemplate that will act as the UI for the category editor.  The DataTemplate will have its DataContext set to a CategoryEntry.

The TargetCategory property identifies which category this category editor applies to.

namespace CiderControls.Common.VisualStudio.Design.Controls {

  /// <summary>
  /// A category editor is a group of properties that can be edited together.
  /// 
  /// The Text and Brushes category editors are examples of a category editor.
  /// The Text category editor consumes all the Text related properties and 
  /// displays them in a complex UI.  The brush category editor consumes all the 
  /// properties of type brush, displays UI and a list of properties to manipulate.
  /// </summary>
  internal class FeedbackControlCategoryEditor : CategoryEditor {

    public FeedbackControlCategoryEditor() {
    }

    /// <summary>
    /// Provides ability to list or not list properties by name in a category.
    /// </summary>
    /// <param name="propertyEntry"></param>
    /// <returns>return true if the property is edited by this category editor.
    /// Returning false will cause the property to be listed as a separate row
    /// within the category.</returns>
    public override bool ConsumesProperty(PropertyEntry propertyEntry) {
      return true;
    }

    /// <summary>
    /// Caegory editor data template
    /// </summary>
    public override DataTemplate EditorTemplate {
      get {
        return FeedbackControlResourceDictionary.Instance.FeedbackCategoryEditor;
      }
    }

    /// <summary>
    /// Used by Blend only
    /// </summary>
    /// <param name="desiredSize"></param>
    /// <returns></returns>
    public override object GetImage(Size desiredSize) {
      return null;
    }

    /// <summary>
    /// Defines the category this category editor is for.
    /// </summary>
    public override string TargetCategory {
      get { return Constants.STR_CUSTOM; }
    }
  }
}

EditorTemplate

The EditorTemplate is a DataTemplate stored in a resource dictionary.  The below code-behind file for the FeedbackControlResourceDictionary demonstrates one technique for conserving resources at design-time by exposing the resource dictionary as a Singleton and also how to expose one or more DataTemplates as strongly typed properties.

The above EditorTemplate property illustrates strong type access to the FeedbackCategoryEditor DataTemplate through the Instance property.  The Instance property provides Singleton design pattern access to the resource dictionary.

If the resource dictionary has more that one DataTemplate, you would add additional properties to expose additional DataTemplates. 

Notice the constant STR_FEEDBACKCATEGORYEDITORTEMPLATE is used in the FeedbackCategoryEditor property.  This same constant is also used in the below FeedbackControlResourceDictionary DataTemplate Key.  Using constants improves the maintainability your code by removing quoted strings, while IntelliSense provides quick access to your defined constants when editing code.

namespace CiderControls.Common.VisualStudio.Design.Controls {

  /// <summary>
  /// The FeedBackCategoryEditor property supplies a strong typed access 
  /// to the FeedBackCategoryEditor DataTemplate located in the 
  /// FeedbackControlResourceDictionary.xaml file.
  ///    
  /// This class implements the Singleton pattern so that the resource 
  /// dictionary is only created once and is accessed through the Instance property.
  /// </summary>
  internal partial class FeedbackControlResourceDictionary : ResourceDictionary {

    [ThreadStatic]
    private static FeedbackControlResourceDictionary _instance;

    private FeedbackControlResourceDictionary() {
      InitializeComponent();
    }

    /// <summary>
    /// return a cached copy of the resource dictionary
    /// </summary>
    internal static FeedbackControlResourceDictionary Instance {
      get {
        if (_instance == null) {
          _instance = new FeedbackControlResourceDictionary();
        }
        return _instance;
      }
    }

    /// <summary>
    /// DataTemplate for the FeedbackCategoryEditor
    /// </summary>
    public DataTemplate FeedbackCategoryEditor {
      get {
        return this[Constants.STR_FEEDBACKCATEGORYEDITORTEMPLATE] as DataTemplate;
      }
    }
  }
}
Category Editor DataTemplate

The below DataTemplate is a Grid with four rows each containing a PropertyContainer control.

A PropertyContainer control gives you several options for specifying the type of UI editor you want for each property.  I have chosen to implement the UI using an InlineRowTemplate.

In the below XAML you’ll see Binding MarkupExtensions with ( ) and  [  ] used when assigning the Path.  If these are unfamiliar to you, please read the MSDN topic Binding Declarations Overview.   (  ) used in a Path is for attached properties.  [  ]  used in a Path is for indexers.

<DataTemplate 
  x:Key="{x:Static c:Constants.STR_FEEDBACKCATEGORYEDITORTEMPLATE}">
  
  <Border Background="Wheat" Padding="6,0,6,6">
    <Grid>
      <Grid.Resources>
        <Style TargetType="{x:Type TextBlock}">
          <Setter Property="FontSize" Value="10" />
          <Setter Property="VerticalAlignment" Value="Bottom" />
        </Style>

        <!-- this ControlTemple is used by all the properties at the bottom of this file -->
        <ControlTemplate x:Key="editorTemplate">
          <Grid 
            Margin="0,7,0,0" 
            DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}, 
                         Path=(mwdpe:PropertyContainer.OwningPropertyContainer)}">
            <Grid.RowDefinitions>
              <RowDefinition Height="Auto" />
              <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <TextBlock Grid.Row="0" Text="{Binding Path=PropertyEntry.PropertyName}" />
            <mwdpe:PropertyMarker Grid.Row="1" VerticalAlignment="Center" 
                                  HorizontalAlignment="Left" />
            <ContentPresenter 
              Margin="20,0,0,0" Grid.Row="1" VerticalAlignment="Center" 
              Content="{Binding Path=PropertyEntry.PropertyValue}" 
              ContentTemplate="{Binding RelativeSource={RelativeSource Self}, 
              Path=(mwdpe:PropertyContainer.OwningPropertyContainer).InlineEditorTemplate}" />
          </Grid>
        </ControlTemplate>

      </Grid.Resources>

      <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
      </Grid.RowDefinitions>

      <mwdpe:PropertyContainer Grid.Row="0" InlineRowTemplate="{StaticResource editorTemplate}" 
                               PropertyEntry="{Binding Path=[Header]}" />

      <mwdpe:PropertyContainer Grid.Row="1" InlineRowTemplate="{StaticResource editorTemplate}" 
                               PropertyEntry="{Binding Path=[CommentHeading]}" />

      <mwdpe:PropertyContainer Grid.Row="2" InlineRowTemplate="{StaticResource editorTemplate}" 
                               PropertyEntry="{Binding Path=[Comment]}" />

      <mwdpe:PropertyContainer Grid.Row="3" InlineRowTemplate="{StaticResource editorTemplate}" 
                               PropertyEntry="{Binding Path=[Value]}" />

    </Grid>
  </Border>
</DataTemplate>

The PropertyEntry establishes the context for the editors exposed by the PropertyContainer. Noticed that the property name in the Binding Path is surrounded with [  ], indicating that this is a indexer.

The InlineRowTemplate property takes a ControlTemplate that determines the UI for the property specified by the PropertyEntry.

Have a look at the DataContext property for the Grid. The PropertyContainer OwningPropertyContainer attached property establishes the PropertyContainer as the DataContext, giving the UI elements within the Grid, access to the PropertyContainer.

In Grid Row 0 the property name is displayed.

In Grid Row 1 the PropertyMarker and the InlinePropertyEditor are displayed. 

Yes, that’s all the required code to get all the functionality of the PropertyMarker.  Note: this currently the only way to surface the features exposed by the PropertyMarker like the Data Binding Builder in your custom editors.

The ContentPresenter is used to render the UI that edits the property value.  In the above ContentPresenter I’m using the current InlineEditorTemplate assigned to the property.  The first three properties in the category editor all use a TextBox for editing their values.  The last property Value uses the Rating control.  See next section for how the Rating control is assigned as the InlineEditorTemplate.

Property Value Editor

The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata to assign the Rating control as the PropertyValueEditor for the Feedback control’s Value property.

AddMemberAttributes(feebackType,
    Constants.STR_VALUE,
    new CategoryAttribute(Constants.STR_CUSTOM),
    PropertyValueEditor.CreateEditorAttribute(typeof(RatingSelectorInlineEditor)));

To implement a property value editor your class must derive from PropertyValueEditor.  In the constructor assign the InlineEditorTemplate property to a DataTemplate that has your value editor.  In the code I’m using the now familiar Singleton pattern to access the RatingEditorResourceDictionary’s RatingSelector DataTemplate.

namespace CiderControls.Common.VisualStudio.Design.Controls {

  /// <summary>
  /// Rating control property value editor
  /// </summary>
  internal class RatingSelectorInlineEditor : PropertyValueEditor {

    public RatingSelectorInlineEditor() {
      this.InlineEditorTemplate = 
        RatingEditorResourceDictionary.Instance.RatingSelector;
    }
  }
}

The RatingSelector DataTemplate exposes the Rating control as its UI.  Notice the TwoWay binding mode.

<!-- Using the Rating Control for the Value property inline editor. -->
<DataTemplate x:Key="{x:Static c:Constants.STR_RATESELECTORTEMPLATE}">
    <cc:Rating Value="{Binding Path=Value, Mode=TwoWay}" />
</DataTemplate>

StringConverter for Properties of Type Object

The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata to enable string editing of the Header property that is of type object.

Without the TypeConverterAttribute metadata, the Header property would not be editable in the properties window.  Assigning a StringConverter enables the properties window TextBox string to be assigned to the Header property value.  Without the StringConverter the developer will get an error when trying to edit the Header property value using the properties window.

AddMemberAttributes(feebackType,
    Constants.STR_HEADER,
    new CategoryAttribute(Constants.STR_CUSTOM),
    new TypeConverterAttribute(typeof(StringConverter)));

Category Attribute

Properties are assigned to a category by using a CategoryAttribute as in the below code from CiderControlsAttributeTableBuilder.cs.

You should assign all custom properties to a category so that your properties are displayed in the correct category in the properties window.

AddMemberAttributes(feebackType,
    Constants.STR_COMMENTHEADING,
    new CategoryAttribute(Constants.STR_CUSTOM));

Description Attribute

While not used in the sample solution, a DescriptionAttribute can be added to a property’s metadata as in the below code.  The description string will appear in the property name ToolTip when the properties window is in alpha view.  This description will also appear in Expression Blend’s property inspector.

AddMemberAttributes(feebackType,
    Constants.STR_COMMENT,
    new DescriptionAttribute("The comment is filled in by the end user."),
    new CategoryAttribute(Constants.STR_CUSTOM));

ToolBoxBrowseable Attribute

The following line of code from CiderControlsAttributeTableBuilder.cs adds the required metadata to keep the Rating control from appearing in the ToolBox Choose Items dialog. 

You should use this metadata to keep controls that you do not want appearing in the ToolBox Choose Items dialog when a developer navigates to your control assembly and selects it or when your control assembly is in a folder that is loaded in the AssemblyFoldersEx registry key.  In the future I’ll publish a blog post on installing controls into the ToolBox.

AddTypeAttributes(ratingType,
    new ToolboxBrowsableAttribute(false)
    );

You can also use the following alternate syntax taking advantage of the static (Shared for VB) No property that returns a new instance of the ToolBoxBrowseableAttribute with Browseable set to false.

AddTypeAttributes(ratingType,
    ToolboxBrowsableAttribute.No
    );

Downloads

C# Source Code Download

VB.NET Source Code Download

PowerPoint Slides from the Development Tools Ecosystem Summit Presentation

Links

Walkthrough: Creating a Category Editor

Walkthrough: Implementing an Inline Value Editor

How to: Create a Value Editor

Close

The title of this post had the words “Code Sharing” in it.  The above code provides a design-time for both the WPF & Silverlight controls in the sample solution.  I was just thinking how easy it was to forget that this code supports both platforms so easily.

Creating custom design-time experiences for developers using your controls is fun, gives your controls a very professional and polished feel and enables those users to be more productive when creating their applications.

Have a great day,

Just a grain of sand on the worlds beaches.


Extensibility Series – Loading Metadata for Microsoft Controls

December 4, 2009

I have received several requests for information on loading metadata for WPF & Silverlight controls shipped by Microsoft.  For example, a developer wanted to add a design time context menu to a Button control but couldn’t figure out how to add the required metadata.

In my previous post I covered the locating and loading of metadata for custom controls that you authored. 

In this post I’ll cover how to locate and load metadata for controls that you were not the author for.

Locating Metadata

In my previous post I explained that metadata is loaded from assemblies and those assemblies had a specific naming convention and location relative to the control assembly it was providing a design-time for.

In this post I’ll show you how to use a registry entry to provide the location of the assembly that has metadata you want Visual Studio to load.

You will need a separate registry entry for WPF & Silverlight design assemblies.

To add a twist, the registry entry used in Beta2 will be different for all post Beta2 builds.  I’m telling you that now so that you can be prepared and have the code ready for the next release of Visual Studio 2010.

FYI: The XAML Power Toys for Visual Studio 2010 Beta2 Cider Designer design-time I did uses these registry entries to provide design-time metadata for the WPF & Silverlight Grid control.

Beta2 Registry Entries

RegistryOne

RegistryTwo 

The full path of the registry key is below:  (line breaks for readability only)

HKEY_CURRENT_USER\Software\Microsoft\VisualStudio\10.0\DesignerProfiles\
    Microsoft.Windows.Design.Metadata.DeveloperMetadata\
    XAMLPowerToys.VisualStudio.Design, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Note:  For 64 bit operating systems, you must use below modified registry key path:

HKEY_CURRENT_USER\SOFTWARE\Wow6432Node\Microsoft\VisualStudio

The last line is the assembly full name and serves as the unique subkey name under Microsoft.Windows.Design.Metadata.DeveloperMetadata.

The key has three values, Assembly, Class and CodeBase.

Assembly: Provide the full assembly name

Class: Provide the class name that implements IProvideAttributeTable

CodeBase: Provide the full path to the assembly

The above information provides Visual Studio the required information and it will load the metadata.

Post Beta2 Registry Entries

Beginning with all versions of Visual Studio 2010 after Beta2, the registry keys have changed and the following will be used instead of the above.

RegistryThree

RegistryTwo 

The full path of the registry key is below:  (line breaks for readability only)

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\10.0\DesignerPlatforms\
  .NetFramework\Metadata\XamlDesigner\
  XAMLPowerToys.VisualStudio.Design, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Note:  For 64 bit operating systems, you must use below modified registry key path:

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio

The last line is the assembly full name and serves as the unique subkey name under XamlDesigner.

The key has three values, Assembly, Class and CodeBase.

Assembly: Provide the full assembly name

Class: Provide the class name that implements IProvideAttributeTable

CodeBase: Provide the full path to the assembly

The above information provides Visual Studio the required information and it will load the metadata.

Loading Metadata

Once the metadata is located, Visual Studio will then load the metadata.

MetadataLoading

When Visual Studio locates an assembly that implements IProvideAttributeTable, that assembly can load metadata for any types.  This is how the XAML Power Toys for Cider is able to load design-time metadata for the Microsoft Grid control.

Close

Have a great day,

Just a grain of sand on the worlds beaches.


Extensibility Series – WPF & Silverlight Design-Time Code Sharing – Part I

November 20, 2009

This is the first in series of posts called the “Extensibility Series.”  This series will cover writing design-times for WPF & Silverlight custom controls for the WPF & Silverlight Designer for Visual Studio 2010 that target .NET 4.0 and Silverlight 3 and 4.  I will use the term Designer for the rest of this article.

Normally in a series you start with a simple example and article.  For this series I need to start with a level 300-400 example because this article and code is for the Development Tools Ecosystem Summit presentation that Mark Boulter and I did.

The presentation was on how to write a design-time assembly that could be shared between WPF and Silverlight custom controls.

Part I of this post covers Feedback and Rating control, design-time assembly discovery, loading and metadata creation.

Part II of this post will cover the control design-time code.

Tutorial Breadcrumb Trail

When you open the source code solution, be sure to open your Task List window so that you can view and walk the step by step tutorial breadcrumb trail I’ve added to the solution.  When you click on each TODO you’ll be taken to that section of code.

BreadcrumbTrail

Background

For the purpose of this article and the code, our fictitious company name is, Cider Controls.  The first control we have ready for release is our WPF & Silverlight Feedback control.  The Feedback control contains our Rating control that allows a value to be selected at run-time using the mouse.  Our assignment is to write a design-time for Visual Studio 2010 for this control.

Note: Setting the rating value at design-time is probably something the control does not actually need.  However, it serves its purpose by illustrating how to use the extensibility features of the Designer.

WPF & Silverlight Feedback Control

FeedbackControl FeedbackControlClassDiagram

The blue text is the Header property.  This property is of type Object.  Developers can supply simple text or use nested XAML to create any type of Header they want.

The text under the Header is the CommentHeading property which is a String.

The TextBox is bound to the Comment property which is a String.

The . . . . . is the Rating control.  This is bound to the Value property which is an Integer.

The CornerRadius property binds to the Border control in the control template.

The Submit Button is enabled when the Value is greater than zero and the Comment is not empty or null.

Required Design-Time Features

  • Support both the WPF & Silverlight versions of the Feedback control
  • Set control value at design-time using a context menu
  • Set control value at design-time using the Rating control in an adorner
  • Set control value at design-time using the Rating control is the properties window
  • Set all control properties in the properties window
  • Provide a Category Editor for the properties window that allows editing the Feedback control properties assigned to the Custom category
Features
Menu Action

The Feedback Value property is zero.

MenuAction

Adorner

After the Feedback Value property has been set, both the control and adorner display the value.

Adorner

Inline Value Editor

Inline Value editor uses the Rating control just like the Feedback control and adorner.

ValueEditor

Category Editor

CategoryView 

Design-time XAML

Notice the Feedback Header that takes XAML.  You could easily add a Grid or StackPanel and then display and image and text if you wanted.

<cc:Feedback 
    HorizontalAlignment="Left" Grid.Column="1" 
    VerticalAlignment="Top" Height="176" Width="304" 
    CommentHeading="Write a design time for your control today."
    CornerRadius="10"
    Padding="3"
    BorderBrush="DarkGray"
    BorderThickness="3"
    Background="WhiteSmoke" Value="3" Margin="12,24,0,0">

    <cc:Feedback.Header>
        <TextBlock 
            Foreground="Blue" FontSize="16" FontWeight="Bold" 
            Text="Control Design Time Example" />
    </cc:Feedback.Header>

</cc:Feedback>

Design-Time Assembly Locating

Writing a design-time for a control is actually pretty simple and the code tends to be small blocks since each individual feature you add is almost self-contained.

There are two complicated pieces, metadata loading and Toolbox installation.

Simplified:  Metadata loading is the design-time process that Visual Studio uses to discover and associate your design-time code with your control so that the Designer will run it at design-time when you want it to.

I’ll cover Toolbox installation in my next Extensibility Series post.

These two can be complicated because there are different ways to do both, some can involve the registry, some involve multiple assemblies and both depend on a naming convention.  In other words, when you first look at this, you’ll think there are a number of moving parts, but after you’ve done this a few times, you’ll have it down.

Design-Time Load Scenarios
  • WPF Custom Control Designer – (single design assembly)
  • Silverlight Custom Control Designer – (single or two design assemblies)
  • Code Sharing for WPF & Silverlight controls (five design assemblies)

Note:  You can use other techniques for reducing the number of assemblies when doing multi-platform design-times, but they can lead to bugs and confusion due to platform mixing and won’t be discussed here.

For this post I’ll only cover the, “Code Sharing for WPF & Silverlight controls” scenario.  The techniques that I’ll describe here are used in Microsoft production code and simplify code sharing.  In future posts I’ll cover the other design-time load scenarios and techniques.

Design-Time Assembly Naming and Location
Assembly name Notes
CiderControls.WPF.dll Custom Control Run-Time Assembly
CiderControls.WPF.Design.*.dll Loaded by Visual Studio and Blend
\Design\CiderControls.WPF.Design.*.dll Located in the \Design sub folder.  Loaded by Visual Studio and Blend
CiderControls.WPF.VisualStudio.Design.*.dll Loaded only by Visual Studio
CiderControls.WPF.Expression.Design.*.dll Loaded only by Blend
\Design\CiderControls.WPF.VisualStudio.Design.*.dll Loaded only by Visual Studio
\Design\CiderControls.WPF.Expression.Design.*.dll Loaded only by by Blend

Looking closely at the above table, you’ll see a naming pattern.  The control assembly can be named whatever the control author wants.  Typically you’ll see the vendor name followed by the product and possibly version number.

The design-time assemblies must be named according to the above table in order for Visual Studio to load them.

For metadata loading Visual Studio loads the .dll’s in the above order.  Meaning .VisualStudio.Design. qualified .dll’s will load second and overwrite metadata in the .Design dll.

The “*” represents optional additional name text that the design-time assembly author can add.  Normally this is not used.  This provides a way to ship additional design assemblies that follow the above naming convention.  (See next paragraph.)

When the metadata is loaded there is one more consideration that is taken into account.  The version number of the Microsoft.Windows.Design.Extensibility.dll or Microsoft.Windows.Design.Interaction.dll that the design-time assembly is compiled against will be compared to those versions on the target machine.

For readability I’ll use MWD in place of Microsoft.Windows.Design.Extensibility.dll in the next short section.

The Design.*.dll nomenclature allows for multiple design assemblies fitting a pattern such as “foo.Design.*.dll”, one for each version of the MWD interface.  Zero or one assembly fitting a pattern will load:

  • If the MWD version referenced by the design-time assembly has a different major version number than the user’s machine MWD version, then the design-time assembly will not load and is bypassed.
  • If more than one design-time assembly is compatible with the user’s machine MWD version, the Designer loads the version compiled against the highest MWD version that is less than or equal to the user’s machine MWD version.

For instance, if the user’s machine MWD version is 4.1.3.0 and there are 4 design-time assemblies matching the pattern “foo.design*.dll”, each compiled against the below MWD versions, the designer reacts as follows:

  • 3.0.1.0: Will not load.  Incompatible API version.
  • 4.0.1.0: Would load if there were not a higher matching version, but since 4.1.1.0 exists it does not load 
  • 4.1.1.0: Version loaded as it is closest to the current version number without exceeding it
  • 4.3.0.0: Will not load in the Designer as it was built against a newer version of the MWD’s
Metadata and Design Code

The design-time assembly does not have to actually have any design time code in it.  In fact as you’ll see shortly, the two design-time assemblies that our solution has that follow the above naming convention only serve as shims between Visual Studio 2010 and our common WPF and Silverlight design-time assembly that does all the work.

WPF

The below screen shot pictures the WPF run-time custom control folder and contents.

CiderControlsFolderOne

The below screen shot pictures the contents of the \Design\ subfolder.

CiderControlsFolderTwo

In the above image, CiderControls.WPF.VisualStudio.Design.dll is the assembly that Visual Studio will discover and load the design-time metadata from.

CiderControls.Common.VisualStudio.Design.dll is in this folder because it is referenced by CiderControls.WPF.VisualStudio.Design.dll.

CiderControls.WPF.dll is in this folder because it is referenced by the common design-time assembly CiderControls.Common.VisualStudio.Design.dll.

Silverlight

The below screen shot pictures the Silverlight run-time custom control folder and contents.

CiderControlsSLFolderOne

The below screen shot pictures the contents of the \Design\ subfolder.

CiderControlsSLFolderTwo

In the above image, CiderControls.Silverlight.VisualStudio.Design.dll is the assembly that Visual Studio will discover and load the design-time metadata from.

CiderControls.Common.VisualStudio.Design.dll is in this folder because it is referenced by CiderControls.Silverlight.VisualStudio.Design.dll.

CiderControls.Silverlight.VisualStudio.Design.Types.dll is in this folder because it is referenced by CiderControls.Common.VisualStudio.Design.dll and CiderControls.Silverlight.VisualStudio.Design.dll.

The above two file references are not a project references but a file or binary references.  When this reference is added, the browse button is used to browse to the CiderControls.Silverlight.dll and the reference added to the file.

CiderControls.Silverlight.dll is in this folder because it is referenced by CiderControls.Silverlight.VisualStudio.Design.Types.dll.

CiderControls.WPF.dll is in this folder because it is referenced by the common design-time assembly CiderControls.Common.VisualStudio.Design.dll.

Note: CiderControls.Silverlight.dll and CiderControls.Silverlight.VisualStudio.Design.Types.dll are the only two assemblies that actually reference Silverlight, the remaining assemblies are all WPF assemblies.

Design Subfolder

In both of the above series of images, the \Design subfolder is under the \Bin\Debug folder for each custom control assembly.  During development, the best way to populate the \Design subfolder is to set up a Post-build event.  Now your design-time test projects only need to reference the design-time assembly and Visual Studio will automatically load your design-time assembly for you.

For the CiderControls.WPF.VisualStudio.Design.dll project I’m using the following Post-build event command line:

    xcopy "$(TargetDir)*.*"   "$(SolutionDir)\CiderControls.WPF\Bin\Debug\Design" /S /I /Y

For the CiderControls.Silverlight.VisualStudio.Design.dll project I’m using the following Post-build event command line:

    xcopy "$(TargetDir)*.*"   "$(SolutionDir)\CiderControls.Silverlight\Bin\Debug\Design" /S /I /Y

WPF Metadata Loading

Visual Studio locating your correctly named design-time assembly is step one.  That assembly must also decorated with the ProvideMetadata attribute:

  [assembly: ProvideMetadata(typeof(RegisterMetadata))]

The ProvideMetadata constructor requires that a type be passed in.  That type must implement the IProvideAttributeTable interface.

IProvideAttributeTable interface has a single read-only property AttributeTable that returns an AttributeTable.

Below is the RegisterMetadata class that is in the CiderControls.WPF.VisualStudio.Design project.

using CiderControls.Common.VisualStudio.Design.Registration;
using CiderControls.WPF.VisualStudio.Design;
using Microsoft.Windows.Design.Metadata;

[assembly: ProvideMetadata(typeof(RegisterMetadata))]

namespace CiderControls.WPF.VisualStudio.Design {

  internal class RegisterMetadata : IProvideAttributeTable {

    AttributeTable IProvideAttributeTable.AttributeTable {
      get {
        CiderControlsAttributeTableBuilder builder =
            new CiderControlsAttributeTableBuilder(new WPFTypeResolver());
        return builder.CreateTable();
      }
    }
  }
}

Within the AttributeTable property we instantiate the CiderControlsAttributeTableBuilder class that derives from AttributeTableBuilder.  AttributeTableBuilder provides the CreateTable method.

The CiderControlsAttributeTableBuilder is located in the “platform neutral” CiderControls.Common.VisualStudio.Design assembly.

The WPFTypeResolver is passed into the CiderControlsAttributeTableBuilder constructor.  This class allows the “platform neutral” CiderControls.Common.VisualStudio.Design assembly to resolve WPF types in our run-time control assembly by referring to them with platform neutral TypeIdentifiers.  The WPFTypeResolver is located in the CiderControls.WPF.VisualStudio.Design project.

The CiderControlsAttributeTableBuilder is the platform neutral class that does all the heavy lifting for creating the required AttributeTable; also know as our metadata.

Below is the WPFTypeResolver class that is in the CiderControls.WPF.VisualStudio.Design project.  The GetPlatformType method takes a platform neutral TypeIdentifier and returns a platform specific type.

using System;
using CiderControls.Common.VisualStudio.Design.Infrastructure;
using CiderControls.Common.VisualStudio.Design.Registration;
using Microsoft.Windows.Design.Metadata;

namespace CiderControls.WPF.VisualStudio.Design {

  internal class WPFTypeResolver : RegistrationTypeResolver {

    public override Type GetPlatformType(TypeIdentifier id) {
      switch (id.Name) {

        case Constants.STR_CIDERCONTROLSFEEDBACK:
          return typeof(CiderControls.Feedback);

        case Constants.STR_CIDERCONTROLSRATING:
          return typeof(CiderControls.Rating);
      }

      throw new ArgumentOutOfRangeException("id.Name", id.Name, message...);
    }
  }
}

Silverlight Metadata Loading

Visual Studio locating your correctly named design-time assembly is step one.  That assembly must also decorated with the ProvideMetadata attribute:

  [assembly: ProvideMetadata(typeof(RegisterMetadata))]

The ProvideMetadata constructor requires that a type be passed in.  That type must implement the IProvideAttributeTable interface.

IProvideAttributeTable interface has a single read-only property AttributeTable that returns an AttributeTable.

Below is the RegisterMetadata class that is in the CiderControls.Silverlight.VisualStudio.Design project.

using CiderControls.Common.VisualStudio.Design.Registration;
using CiderControls.Silverlight.VisualStudio.Design;
using Microsoft.Windows.Design.Metadata;

[assembly: ProvideMetadata(typeof(RegisterMetadata))]

namespace CiderControls.Silverlight.VisualStudio.Design {

  internal class RegisterMetadata : IProvideAttributeTable {

    AttributeTable IProvideAttributeTable.AttributeTable {
      get {
        CiderControlsAttributeTableBuilder builder =
          new CiderControlsAttributeTableBuilder(new SilverlightTypeResolver());
        return builder.CreateTable();
      }
    }
  }
}

Within the AttributeTable property we instantiate the CiderControlsAttributeTableBuilder class that derives from AttributeTableBuilder.  AttributeTableBuilder provides the CreateTable method.

The CiderControlsAttributeTableBuilder is located in the “platform neutral” CiderControls.Common.VisualStudio.Design assembly.

The SilverlightTypeResolver is passed into the CiderControlsAttributeTableBuilder constructor.  This class allows the “platform neutral” CiderControls.Common.VisualStudio.Design assembly to resolve Silverlight types in our run-time control assembly by referring to them with platform neutral TypeIdentifiers.  The SilverlightTypeResolver is located in the CiderControls.Silverlight.VisualStudio.Design project.

The CiderControlsAttributeTableBuilder is the platform neutral class that does all the heavy lifting for creating the required AttributeTable; also know as our metadata.

Below is the SilverlightTypeResolver class that is in the CiderControls.Silverlight.VisualStudio.Design project.  The GetPlatformType method takes a platform neutral TypeIdentifier and returns a platform specific type.

using System;
using CiderControls.Common.VisualStudio.Design.Infrastructure;
using CiderControls.Common.VisualStudio.Design.Registration;
using CiderControls.Silverlight.VisualStudio.Design.Types;
using Microsoft.Windows.Design.Metadata;

namespace CiderControls.Silverlight.VisualStudio.Design {

  internal class SilverlightTypeResolver : RegistrationTypeResolver {

    public override Type GetPlatformType(TypeIdentifier id) {
      switch (id.Name) {

        case Constants.STR_CIDERCONTROLSFEEDBACK:
          return SilverlightTypes.FeedbackControlType;

        case Constants.STR_CIDERCONTROLSRATING:
          return SilverlightTypes.RatingControlType;

      }

      throw new ArgumentOutOfRangeException("id.Name", id.Name, message...);
    }
  }
}

This assembly, CiderControls.Silverlight.VisualStudio.Design is a WPF assembly.  Notice that we are not directly returning a Silverlight type by using the typeof method.  Instead we are returning a type that is located in the CiderControls.Silverlight.VisualStudio.Design.Types assembly and is exposed by static properties in the SivlerlightTypes class below.

The CiderControls.Silverlight.VisualStudio.Design.Types assembly has a project reference to the Sivlerlight controls run-time assembly.

using System;

[assembly:
  System.Runtime.CompilerServices.InternalsVisibleTo("CiderControls.Silverlight.VisualStudio.Design")]

[assembly:
  System.Runtime.CompilerServices.InternalsVisibleTo("CiderControls.Common.VisualStudio.Design")]

namespace CiderControls.Silverlight.VisualStudio.Design.Types {

  internal class SilverlightTypes {

    public static readonly Type FeedbackControlType = typeof(CiderControls.Feedback);
    public static readonly Type RatingControlType = typeof(CiderControls.Rating);
  }
}

I’ve made use of the Friend Assemblies feature that allows all my classes in the solution to be internal.  For VB.NET they will all be scoped as Friend.

Silverlight Type Resolution

By now you understand that the CiderControlsAttributeTableBuilder is “platform neutral” and is located in the CiderControls.Common.VisualStudio.Design assembly.

You know that the SiverlightTypeResolver will be pass into the CiderControlsAttributeTableBuilder constructor.  CiderControlsAttributeTableBuilder is the same class that the WPFTypeResolver will be passed into also.

When the CiderControlsAttributeTableBuilder needs to resolve a type it will pass a platform neutral TypeIdentifier to one of the TypeResolvers, they in turn will return a platform specific type.

For Silverlight only, the SiverlightTypeResolver will use the SilverlightTypes class to perform the actual type resolution.

While this seems confusing at first, it is the best way to allow Silverlight types to be resolved in a platform neutral fashion and without polluting a WPF assembly with Silverlight types.

Metadata Building

CiderControlsAttributeTableBuilder does the heavy lifting and is responsible for creating the AttributeTable required by Visual Studio.  This code is platform neutral, meaning it returns an AttributeTable for WPF or Silverlight without having to directly reference the run-time assembly controls inside its code.

For the below code, I left the comments in place to make reading a large section of code much easier.  The comments inline explain what is going on.

You’ll notice how the WPFTypeResolver and the SiverlightTypeResolver are used in this code.  The module level variable _registrationTypeResolver holds a reference to the TypeResolver that is used to resolve platform specific types using the platform neutral TypeIdentifier.

using System;
using System.ComponentModel;
using CiderControls.Common.VisualStudio.Design.Controls;
using CiderControls.Common.VisualStudio.Design.Infrastructure;
using Microsoft.Windows.Design.Features;
using Microsoft.Windows.Design.Metadata;
using Microsoft.Windows.Design.PropertyEditing;
using Microsoft.Windows.Design;

//
// This should be in AssemblyInfo.cs - declaring here to make it obvious
//

[assembly:
  System.Runtime.CompilerServices.InternalsVisibleTo("CiderControls.WPF.VisualStudio.Design")]

[assembly:
  System.Runtime.CompilerServices.InternalsVisibleTo("CiderControls.Silverlight.VisualStudio.Design")]

namespace CiderControls.Common.VisualStudio.Design.Registration {

  //TODO  5 - CiderControlsAttributeTableBuilder

  /// <summary>
  /// Platform neutral class that builds platform specific metadata
  /// </summary>
  internal class CiderControlsAttributeTableBuilder : AttributeTableBuilder {

    // allows for Platform specific Types to be used without directly referencing Silverlight
    private RegistrationTypeResolver _registrationTypeResolver;

    //TODO  6 - registrationTypeResolver resolves the platform specific Types in the platform neutral 
    //          metadata builder
    public CiderControlsAttributeTableBuilder(RegistrationTypeResolver registrationTypeResolver) {
      _registrationTypeResolver = registrationTypeResolver;

      AddFeedbackControlAttributes();
      AddRatingControlAttributes();
    }

    /// <summary>
    /// Builds Feedback control metedata
    /// </summary>
    private void AddFeedbackControlAttributes() {

      //TODO  7 - Resolve the Platform specific Feedback control from the platform neutral 
      //          TypeIdentifier
      Type feebackType = _registrationTypeResolver.GetPlatformType(MyPlatformTypes.Feedback.TypeId);

      //TODO  9 - Using the above feedbackType, create platform specific metadata for the Feedback 
      //          control the below code adds features, catetory editor, inline editor, type converter
      //          and assigns a Category to each Feedback control property

      AddTypeAttributes(feebackType,
          new FeatureAttribute(typeof(FeedbackControlInitializer)),
          new FeatureAttribute(typeof(FeedbackControlContextMenuProvider)),
          new FeatureAttribute(typeof(FeedbackControlAdornerProvider))
          );

      AddCategoryEditor(feebackType,
          typeof(FeedbackControlCategoryEditor));

      AddMemberAttributes(feebackType,
          Constants.STR_CORNERRADIUS,
          new CategoryAttribute(Constants.STR_COMMON));

      // since the below Header property is of type object, it requires this 
      // StringConverter to enable editing of simple string values in the properties window.
      AddMemberAttributes(feebackType,
          Constants.STR_HEADER,
          new CategoryAttribute(Constants.STR_CUSTOM),
          new TypeConverterAttribute(typeof(StringConverter)));

      AddMemberAttributes(feebackType,
          Constants.STR_VALUE,
          new CategoryAttribute(Constants.STR_CUSTOM),
          PropertyValueEditor.CreateEditorAttribute(typeof(RatingSelectorInlineEditor)));

      AddMemberAttributes(feebackType,
          Constants.STR_COMMENT,
          new CategoryAttribute(Constants.STR_CUSTOM));

      AddMemberAttributes(feebackType,
          Constants.STR_COMMENTHEADING,
          new CategoryAttribute(Constants.STR_CUSTOM));

    }

    /// <summary>
    /// Builds Rating control metedata
    /// </summary>
    private void AddRatingControlAttributes() {

      //TODO  7.1 - Resolve the Platform specific Rating control from the platform neutral 
      //            TypeIdentifier
      Type ratingType = _registrationTypeResolver.GetPlatformType(MyPlatformTypes.Rating.TypeId);

      //TODO  9.1 - Using the above ratingType, create platform specific metadata for the Rating 
      //            control the below code keeps the Rating control from appearing in the 
      //            Choose Items dialog after the CiderControls.WPF or CiderControls.Siverlight 
      //            assembly is added.
      //
      // this is only done for illustration, there is no actual reason to keep the Rating control
      // out of the Choose Items dialog.
      AddTypeAttributes(ratingType,
          new ToolboxBrowsableAttribute(false)
          );
    }

    private void AddTypeAttributes(Type type, params Attribute[] attribs) {
      base.AddCallback(type, builder => builder.AddCustomAttributes(attribs));
    }

    private void AddCategoryEditor(Type type, Type editorType) {
      base.AddCallback(type, builder => 
         builder.AddCustomAttributes(CategoryEditor.CreateEditorAttribute(editorType)));
    }

    private void AddMemberAttributes(Type type, string memberName, params Attribute[] attribs) {
      base.AddCallback(type, builder => builder.AddCustomAttributes(memberName, attribs));
    }
  }
}

The last three methods are helper methods that make the metadata code cleaner and easier to read.

In the next Part II of this article, I’ll tie the above attributes to each of the design-time features.

Downloads

C# Source Code Download

VB.NET Source Code Download

PowerPoint Slides from the Development Tools Ecosystem Summit Presentation

Online Documentation Status

Almost all of MSDN and most examples you’ll see on the Internet use the interfaces from Visual Studio 2008.  When you see IRegisterMetadata used in an example, you know you’re looking at old code.  Overtime this will change, new examples will be posted and MSDN updated.

Remember IRegisterMetadata has been replaced with IProvideAttributeTable in Visual Studio 2010.  Most of old the code and the substance of the example or documentation you are viewing is probably correct and good to learn from.  You just need to be aware that you are looking at older code and that example won’t compile under Visual Studio 2010.

More Information Links

MSDN: WPF Designer Extensibility  (This is the best extensibility reference.)

WPF and Silverlight Designer Extensibility Samples

Ning Zhang’s Blog: Silverlight Design Time: Toolkit October 2009 Release Update

How to Write Silverlight Design Time for All Designers: Visual Studio 2008, Blend 2; Blend 3, and Visual Studio 2010

Close

Hope each of you have a wonderful Thanksgiving.

I’m going out to sea again to write code for a week.  You can follow my cruise Tweets @kdawg02.

Have a great day,

Just a grain of sand on the worlds beaches.


Follow

Get every new post delivered to your Inbox.

Join 241 other followers