Click here to Skip to main content
Click here to Skip to main content

Calcium: A Modular Application Toolset Leveraging PRISM – Part 2

, 23 Nov 2009 BSD
Rate this:
Please Sign up or sign in to vote.
Calcium provides much of what one needs to rapidly build a multifaceted and sophisticated modular application. Includes a host of modules and services, and an infrastructure that is ready to use in your next application.
Calcium Logo

Contents

Introduction

Calcium is a WPF composite application toolset that leverages the Composite Application Library. It provides much of what one needs to rapidly build a multifaceted and sophisticated modular application.

In part one of this series, we explored some of Calcium’s core infrastructure, including module management, region adaptation, and the Bootstrapper. Now, we will examine the messaging system, and take a look at two other modules, namely the WebBrowser module and the Output module.

Calcium screenshot

Figure: Calcium with the Web browser and Output modules visible.

In the first article, we saw that Calcium consists of a client application and server based WCF services, which allow interaction and communication between clients. Out of the box, Calcium comes with a host of modules and services, and an infrastructure that is ready to use in your next application.

We've got a lot of ground to cover. For that reason, I've decided to break this article up into a series of three, maybe four articles.

  1. Introduction to Calcium, Module Manager
  2. Message Service, WebBrowser module, Output Module (this article)
  3. File Service, View Service, Rebranding Calcium
  4. TBA

In this article, you will learn how to:

  • build a location agnostic messaging system;
  • use WCF custom headers to identify specific client application instances;
  • create web browser and output window modules for Calcium;
  • inject RoutedCommand handlers into the shell (main window).

Some of the contents in this series of articles are not at an advanced level, and will suit even those that are new to Prism. While others, such as the messaging system, will suit more advanced readers. Hopefully, there are things here to learn for everyone.

These series of articles are, in some respects, an introduction to some areas of Prism. Although, if you are completely new to Prism, you may find yourself somewhat overloaded at times, and I would recommend taking a look at some beginner Prism articles before you tackle Calcium, such as Jammer's Introduction or the Technical Concepts on MSDN.

Location Agnostic Message Service

When developing an application, clearly it’s prudent to have uniformity in the manner certain tasks are carried out. An example of such a task is displaying common dialog boxes. But wait, if you think this section is just going to be about an abstracted dialog box system, it isn't. That would be far too boring. While Calcium does provide a common dialog system, it also allows us to display a dialog to the user from the server during any WCF call! Moreover, it allows us to consume the same API on the client and the server. We don't need to worry about interpreting the result of a WCF call in the client. This means we are able to interact with the user directly from anywhere, without having to know where our business logic is executing, i.e., client or server.

Out of the box, Calcium comes with a number of IMessageService implementations. There is a client implementation for WPF, another client-side implementation for command line driven applications, and a server-side implementation that sends messages back to the client via a callback and leverages the client-side IMessageService implementation.

Firstly, I want to provide you with an overview of the client-side message service, and then we will examine how it is leveraged from the server-side to provide the location agnosticism.

Client-side Message Service Implementation

Obviously, it’s unwise for each member of a development team to be creating his or her own dialogs for simple tasks such as asking the user a closed ended question (a Yes/No question box). We would end up with lots of duplication, and that degrades maintainability. If we decide to change the caption in the dialogs across the board, it is rather more difficult if dialogs are scattered throughout the project. Likewise, if we wish to port the application from WPF to Silverlight, or even to a command line interface (think Powershell or mobile applications), it’s great to be able to swap out the implementation for any given scenario. Clearly, an abstracted layer is in order.

The Message Service has various overloads which allow errors, captions, and messages to be specified.

Let’s take a look at the IMessageService interface and client-side implementations.

IMessageService Class Diagram

Figure: IMessageService allows us to interact with the user in a UI agnostic manner.

The Message Service allows for a Message Importance level to be specified. This allows for a threshold noise level to be specified by the user. If the MessageImportance is lower than the user’s preference, then the user won't be bothered with the message. In a later version of Calcium, we shall see a Preferences Service for specifying the preferred level.

MessageServiceBase Class Diagram

Figure: MessageService and CommandLineMessageService both override the ShowCustomDialog method of MessageServiceBase to cater for their particular environments.

By applying variation through merely overriding the ShowCustomDialog method, it also makes it very easy to mock the MessageServiceBase class for testing.

Our client-side WPF implementation channels all messaging requests through the ShowCustomDialog method as shown in the following excerpt:

/// <summary>
/// WPF implementation of the <see cref="IMessageService"/>.
/// </summary>
public class MessageService : MessageServiceBase
{
    public override MessageResult ShowCustomDialog(string message, string caption,
        MessageButton messageButton, MessageImage messageImage, 
        MessageImportance? importanceThreshold, string details)
    {
        /* If the importance threshold has been specified 
         * and it's less than the minimum level required (the filter level) 
         * then we don't show the message. */
        if (importanceThreshold.HasValue && 
		importanceThreshold.Value < MinumumImportance)
        {
            return MessageResult.OK;
        }

        if (MainWindow.Dispatcher.CheckAccess())
        {/* We are on the UI thread, and hence no need to invoke the call.*/
            var messageBoxResult = MessageBox.Show(MainWindow, message, caption, 
            messageButton.TranslateToMessageBoxButton(), 
            messageImage.TranslateToMessageBoxButton());
            return messageBoxResult.TranslateToMessageBoxResult();
        }

        MessageResult result = MessageResult.OK; /* Satisfy compiler 
						with default value. */
        MainWindow.Dispatcher.Invoke((ThreadStart)delegate
            {
                var messageBoxResult = MessageBox.Show(MainWindow, message, caption, 
                messageButton.TranslateToMessageBoxButton(), 
                messageImage.TranslateToMessageBoxButton());
                result = messageBoxResult.TranslateToMessageBoxResult();
            });

        return result;
    }

    static Window MainWindow
    {
        get
        {
            return UnitySingleton.Container.Resolve<IMainWindow>() as Window;
        }
    }
}

I have created various extension methods for translating between the native WPF enums MessageBoxButton, MessageBoxImage, and MessageBoxResult. Why go to all of this trouble? At first glance, it resembles an antipattern. The reason is, in fact, that there are differences in these enums in WPF and Silverlight, and this allows us to cater for both without duplication.

I am considering expanding the service to support message details, and perhaps reducing the number of overloads with a reference type Message parameter. Another improvement would be to implement a Don't show again checkbox system. I leave that for a future incarnation. I hope to use Karl Shifflet’s excellent Common TaskDialog project WPFTaskDialogVistaAndXP.aspx (with Karl's permission).

Server-side Message Service

We've looked at the basic client-side implementation of IMessageService, but now things are going to get a little more interesting. With Calcium, we have the capability to consume the IMessageService in the same manner on both client and server. To accomplish this, we use a WCF custom header and a duplex service callback.

The demonstration application contains a module called the MessageServiceDemoModule. The view for this module contains a single button that becomes enabled when the CommunicationService has connectivity with the server.

The following excerpt shows code from a WCF service, which is executed asynchronously. The user is presented with dialogs on the client-side. Remember, this code executes server-side!

public class DemoService : IDemoService
{
    public void DemonstrateMessageService()
    {
        var messageService = UnitySingleton.Container.Resolve<IMessageService>();
        ThreadPool.QueueUserWorkItem(delegate { 
            messageService.ShowMessage(
            "This is a synchronous message invoked from the server. " 
                + "The service method is still executing and waiting for your response.", 
            "DemoService", MessageImportance.High);
            bool response1 = messageService.AskYesNoQuestion(
              "This is a question invoked from the server. Select either Yes or No.", 
              "DemoService");
            string responseString = response1 ? "Yes" : "No";
            messageService.ShowMessage(string.Format(
                "You selected {0} as a response to the last question.", responseString), 
                MessageImportance.High);
         });
    }
}

In the above excerpt, we retrieve the IMessageService instance from the Unity container. This is done in the same manner as we would on the client! Being location agnostic allows us to move business logic far more easily. I have placed the Message Service calls here inside a QueueUserWorkItem delegate just to demonstrate the independence of the Message Service. The WCF call returns almost immediately, yet a child thread will continue to work in the background and will still retain the capability to communicate with the user. Without using a child thread to communicate with the user, we may experience a timeout if the user fails to respond fast enough. Please note that we are required to retrieve the Message Service from the Unity container before the service call completes. Not doing so will raise an exception, as the OperationContext for the service call will no longer be present.

Screenshot of Demo Message Module, displaying message.

Figure: Server causes dialog to be presented client-side.

Figure: Question asked from server.

Figure: Response received and echoed back to user.

How it all Works

When we need a service channel, we use our IChannelManager instance to retrieve an instance. I've discussed the IChannelManager in other articles, in particular here and here.

In WCF, each WCF service has an independent session state. In other words, WCF services don't share a global session. So, in order to identify the client application instance from any WCF service call, we place a custom header into every service channel we create, as shown in the following excerpt from the ServiceManagerSingleton and InstanceIdHeader classes:

public static class InstanceIdHeader
{
    public static readonly String HeaderName = "InstanceId";
    public static readonly String HeaderNamespace 
            =  OrganizationalConstants.ServiceContractNamespace;
}

We use this static class to place the header, and also to retrieve the header on the server.

void AddCustomHeaders(IClientChannel channel)
{
    MessageHeader shareableInstanceContextHeader = MessageHeader.CreateHeader(
    InstanceIdHeader.HeaderName,
    InstanceIdHeader.HeaderNamespace,
    instanceId.ToString());

    var scope = new OperationContextScope(channel);
    OperationContext.Current.OutgoingMessageHeaders.Add(shareableInstanceContextHeader);
}

So, when we create the service channel, we add the custom header as shown in the following excerpt:

public TChannel GetChannel<TChannel>()
{
    Type serviceType = typeof(TChannel);
    object service;

    channelsLock.EnterUpgradeableReadLock();
    try
    {
        if (!channels.TryGetValue(serviceType, out service))
        {/* Value not in cache, therefore we create it. */
            channelsLock.EnterWriteLock();
            try
            {
                /* We don't cache the factory as it contains a list of channels 
                 * that aren't removed if a fault occurs. */
                var channelFactory = new ChannelFactory<TChannel>("*");

                service = channelFactory.CreateChannel();
                AddCustomHeaders((IClientChannel)service);

                var communicationObject = (ICommunicationObject)service;
                communicationObject.Faulted += OnChannelFaulted;
                channels.Add(serviceType, service);
                communicationObject.Open(); 
                ConnectIfClientService(service, serviceType);

                UnitySingleton.Container.RegisterInstance<TChannel>((TChannel)service);
            }
            finally
            {
                channelsLock.ExitWriteLock();
            }
        }
    }
    finally
    {
        channelsLock.ExitUpgradeableReadLock();
    }

    return (TChannel)service;
}

We cache the channel until it is closed or faults. But as soon as we create a new channel, we add the header to be consumed on the server. We also have duplex channel support, which works in much the same way.

We must talk to the ICommunicationService in order to create a callback, before we attempt to consume the Message Service on the server-side. This is done automatically by the CommunicationModule. In fact, the CommunicationModule polls the server periodically to let it know that it is still alive. It will also detect network connectivity, or lack thereof, and disable or enable the polling accordingly.

/// <summary>
/// Notifies the server communication service that the client is still alive.
/// Occurs on a ThreadPool thread. <see cref="NotifyAlive"/>
/// </summary>
/// <param name="state">The unused state.</param>
void NotifyAliveAux(object state)
{
    try
    {
        if (!NetworkInterface.GetIsNetworkAvailable())
        {
            return;
        }
        var channelManager = UnitySingleton.Container.Resolve<IChannelManager>();
        var communicationService = 
            channelManager.GetDuplexChannel<ICommunicationService>(callback);
        communicationService.NotifyAlive();
        if (!connected)
        {
            connected = true;
            connectionEvent.Publish(ConnectionState.Connected);
        }
        lastExceptionType = null;
    }
    catch (Exception ex)
    {
        Type exceptionType = ex.GetType();
        if (exceptionType != lastExceptionType)
        {
            Log.Warn("Unable to connect to communication service.", ex);
        }
        lastExceptionType = exceptionType;
        if (connected)
        {
            connected = false;
            connectionEvent.Publish(ConnectionState.Disconnected);
        }
    }
    finally
    {
        connecting = false;
    }
}

As an aside, the CommunicationModule itself has no UI. Its purpose is to interact with the CommunicationService, and to provide notifications to the client when various server-side events take place. One such CompositeEvent is the ConnectionEvent, which can be seen in the previous excerpt.

I plan on expanding the Message Service system to allow text input, drop down list selection, and maybe even custom dialogs.

WebBrowser Module

The WebBrowser module consists of the module itself, a view, and a viewmodel. The view, a class called WebBrowserView, plays host to a WPF WebBrowser control, whose Url property is bound to the ViewModel, as shown in the following excerpt:

<WebBrowser Name="WebBrowser" wb:WebBrowserUtility.BindableSource="{Binding Url}"
   HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />

Figure: Screenshot of Calcium with WebBrowser on show.

The WebBrowser toolbar has a button whose Command is the WebBrowserViewModel.NavigateCommand and a TextBox. The NavigateCommand is actually injected into the shell’s CommandBinding collection so that it may be invoked when the current view implements the content interface IWebBrowserView.

This can be seen in action in the WebBrowserModule, where we associate the WebBrowserViewModel.NavigateCommand with the content type WebBrowserViewModel, as shown in the following excerpt:

/* When a WebBrowserViewModel is the ViewModel of the active item in the shell,
 * the NavigateCommand becomes active. */
commandService.AddCommandBindingForContentType<WebBrowserViewModel>(
WebBrowserViewModel.NavigateCommand,
(arg, commandParameter) => arg.Navigate(commandParameter),
(arg, commandParameter) => arg.CanNavigate(commandParameter));

Here, the shell accepts an ICommand, and when a WebBrowserViewModel becomes active, it will enable the command according to the CanNavigate handler.

The following excerpt is from the ICommandService interface, in which we see the various signatures for associating commands.

/// <summary>
/// Adds a <seealso cref="CommandBinding"/> for the shell, 
/// that is associated with the active content.
/// When the command binding's canExecute handler is called, 
/// we get the active content <strong>ß</strong> in the shell.
/// If the active content is of the specified <code>TContent</code> 
/// type then the specified <code>canExecuteHandler(ß)</code> is called.
/// When the command binding's execute handler is called, 
/// we get the active content <strong>ß</strong> in the shell.
/// If the active content is of the specified <code>TContent</code> 
/// type then the specified <code>executeHandler(ß)</code> is called.
/// </summary>
/// <typeparam name="TContent">The type of the content 
/// that must be active in the workspace.</typeparam>
/// <param name="command">The command to register.</param>
/// <param name="executeHandler">The execute handler. 
/// Must return <code>true</code> if the command 
/// is to be marked as handled.</param>
/// <param name="canExecuteHandler">The can execute handler. 
/// If the handler returns <code>true</code> the command is executable 
/// (e.CanExecute is set to <code>true</code>), otherwise the command 
/// will be not executable (e.CanExecute is set to <code>false</code>.</param>
void AddCommandBindingForContentType<TContent>
				(ICommand command,
                           		Func<TContent, object, bool> executeHandler,
                          		Func<TContent, object, bool> canExecuteHandler)
    where TContent : class;

//… overloads omitted for brevity.

void AddCommandBinding(ICommand command,
Func<bool> executeHandler, Func<bool> canExecuteHandler);

void AddCommandBinding(ICommand command,
Func<bool> executeHandler, Func<bool> canExecuteHandler, KeyGesture keyGesture);

void RegisterKeyGester(KeyGesture keyGesture, ICommand command);

void RemoveCommandBinding(ICommand command);

The WPF routed command infrastructure is useful in scenarios where we have a command target located within the visual tree. Yet with an interface that combines tool views and document views, it can be difficult triggering command handlers, and some controls may behave not as expected. This can be demonstrated when e.g., toolbar buttons are not enabled when the current workspace view is not focused.

In order to address this challenge, we use an ICommandService method to associate a known content type with a command. The following excerpt shows the AddCommandBindingForContentType interface definition.

The AddCommandBindingForContentType has been implemented in the DesktopShell.Commanding.cs as the following excerpt demonstrates:

public void AddCommandBindingForContentType<TContent>(
    ICommand command,
    Func<TContent, object, bool> executeHandler, 
    Func<TContent, object, bool> canExecuteHandler)
    where TContent : class
{
    /* When the workspace view changes, 
     * if the view is viewType then the specified command 
     * will be enabled depending on the result of the command.CanExecute. 
     * When the command is executed the current view's specified member 
     * will be called. */
    CommandBindings.Add(new CommandBinding(command,
        (sender, e) =>
        {
            var content = GetSelectedContent<TContent>();
            if (content == null)
            {
                /* Shouldn't get here because the CanExecute handler 
                 * should prevent it. */
                return;
            }
            e.Handled = executeHandler(content, e.Parameter);
        },
        (sender, e) =>
        {
            var content = GetSelectedContent<TContent>();
            if (content == null)
            {
                e.CanExecute = false;
                return;
            }
            e.CanExecute = canExecuteHandler(content, e.Parameter);
        }));
}

TabItemDictionary.xaml:

<DataTemplate x:Key="TabHeaderDataTemplate">
<StackPanel Orientation="Horizontal" VerticalAlignment="Stretch">
<TextBlock x:Name="textBlock" 
   Text="{Binding TabHeader}" 
   HorizontalAlignment="Left" TextTrimming="CharacterEllipsis" 
   TextWrapping="NoWrap" Foreground="#FFFFFFFF" Margin="0,2,0,0"  
   FontFamily="Arial" FontSize="11" />
<TextBlock Text="*" 
   Visibility="{Binding Path=Content.Dirty, FallbackValue=Collapsed, 
   Converter={StaticResource BooleanToVisibilityConverter}}"
   HorizontalAlignment="Left" 
   Foreground="#FFFFFFFF" Margin="0,2,0,0" />
<Button x:Name="button"  
    Command="ApplicationCommands.Close" CommandParameter="{Binding Path=View}" 
    Template="{DynamicResource CloseTabButtonControlTemplate}" 
    Background="{x:Null}" BorderBrush="{x:Null}" Foreground="{x:Null}" 
    Width="9" Height="9" Opacity="1" ToolTip="Close" 
    Margin="8,3,0,0" VerticalAlignment="Stretch" BorderThickness="0"
    HorizontalAlignment="Right" />
</StackPanel>
</DataTemplate>

This all comes together in the WebBrowserModule class, which takes the Workspace region from the RegionManager and populates it with a WebBrowserView. We then create a new instance of the WebBrowserToolBar and place it in the StandardToolBarTray region of the shell.

[Module(ModuleName = ModuleNames.WebBrowser)]
public class WebBrowserModule : IModule
{
    public void Initialize()
    {
        var regionManager = UnitySingleton.Container.Resolve<IRegionManager>();
        var view = new WebBrowserView();
        regionManager.Regions[RegionNames.Workspace].Add(view);
        string startUrl = "http://wpfdisciples.wordpress.com/";
        var viewModel = (WebBrowserViewModel)view.ViewModel;
        viewModel.Url = startUrl;

        /* Add web browser toolbar. */
        var viewService = UnitySingleton.Container.Resolve<IViewService>();
        var toolBarProvider = new WebBrowserToolBar { Url = startUrl };
        var toolBar = toolBarProvider.ToolBar;
        regionManager.Regions[RegionNames.StandardToolBarTray].Add(toolBar);
        viewService.AssociateVisibility(typeof(IWebBrowserView),
            new UIElementAdapter(toolBar), Visibility.Collapsed);

        var shell = UnitySingleton.Container.Resolve<IShell>();

        /* When a WebBrowserViewModel is the ViewModel of the active item in the shell,
         * the NavigateCommand becomes active. */
        shell.AddCommandBindingForContentType<WebBrowserViewModel>(
            WebBrowserViewModel.NavigateCommand,
            (arg, commandParameter) => arg.Navigate(commandParameter),
            (arg, commandParameter) => arg.CanNavigate(commandParameter));
    }
}

In order to hide and show the toolbar according to content present in the interface, we use the IViewService, which will hide the toolbar when the current content does not implement IWebBrowser view, and show it when it does. We will examine View Service in the next article in this series.

The final thing we do in the WebBrowserModule is to add the command binding to the shell.

The reader may notice that throughout my code I generally avoid using constructor injection. What is constructor injection? Constructor injection is where a Dependency Injection (DI) container, in this case Unity, is used to automatically call a non default constructor and supply it with instances of known types. I choose to avoid it because I have found it to be troublesome. When resolution failures occur, and one has a number of cascading types that are resolved during constructor injection, one may find oneself digging through reams of stacktrace looking for the root cause of the failure. Thus, I normally retrieve the DI container via a singleton. I can't see the value in injecting the container either. To me, it’s just bloat.

Output Module

The Output Module is used to display messages received via the OutputPostedEvent, which is a Prism CompositePresentationEvent. CompositePresentationEvents are used, along with the IEventAggregator, to allow modules to subscribe and publish events in a decoupled manner. If you are new to CompositePresentationEvents (formally CompositeEvent), more information can be found here[^].

Output module screenshot.

Figure: Routed events cause messages to be displayed in OuputView.

This event can be published from anywhere client-side. The OutputViewModel subscribes to this event, and when a message is received, it adds it to an ObservableCollection, as shown in the following excerpt:

class OutputViewModel : ViewModelBase
{
    readonly ObservableCollection<OutputMessage> outputMessages 
                = new ObservableCollection<OutputMessage>();
        
    public ObservableCollection<OutputMessage> OutputMessages
    {
        get
        {
            return outputMessages;
        }
    }

    public OutputViewModel(IOutputView outputView) : base(outputView)
    {
        var eventAggregator = UnitySingleton.Container.Resolve<IEventAggregator>();
        var outputPostedEvent = eventAggregator.GetEvent<OutputPostedEvent>();
        outputPostedEvent.Subscribe(OnOutputPosted);
        TabHeader = "Output";
    }

    void OnOutputPosted(OutputMessage message)
    {
        OutputMessages.Add(message);
    }
}

The View is merely responsible for displaying the messages. We can see how a message can be sent to the Output view in the TextEditorModule.

/* Send an output message. */
var eventAggregator = UnitySingleton.Container.Resolve<IEventAggregator>();
var outputPostedEvent = eventAggregator.GetEvent<OutputPostedEvent>();
outputPostedEvent.Publish(new OutputMessage { 
    Category = outputCategory, Message = fileNameUsed + " opened." });

We first retrieve the IEventAggregator instance from the DI container. Then the event sync is retrieved using the GetEvent method. We then publish, or raise, the event, which then causes the handler OnOutputPosted to be called in the OutputViewModel.

Conclusion

In this article, we have seen how Calcium can be used to provide a location agnostic messaging system, which allows a set of common dialogs to be displayed to the user from anywhere, be it server-side or client-side. We can consume the same API on the client and the server, allowing us to interact with the user directly from anywhere, without having to know where our business logic is executing. We also explored the implementation of a web browser module, an output module, and we examined how it is possible to inject RoutedEvent handlers into Calcium’s shell.

In the next article, we shall be taking a look at the File Service, which automatically handles common IO errors, and which has a rather flexible API that I really like. We will also look at the User Affinity Module for providing feedback about other users of the application, and finally the Text Editor Module, which will tie in many of the other features we have been exploring.

We still have a lot to cover, and I hope you will join me for our next installment.

I hope you find this project useful. If so, then I'd appreciate it if you would rate it and/or leave feedback below. This will help me to make my next article better.

History

  • July 2009
    • Initial publication

License

This article, along with any associated source code and files, is licensed under The BSD License

Share

About the Author

Daniel Vaughan
President Outcoder
Switzerland Switzerland
Daniel Vaughan is a Microsoft MVP and co-founder of Outcoder, a Swiss software and consulting company dedicated to creating best-of-breed user experiences and leading-edge back-end solutions, using the Microsoft stack of technologies--in particular WPF, WinRT, Windows Phone, and also Xamarin.Forms.
 
Daniel is the author of Windows Phone 8 Unleashed and Windows Phone 7.5 Unleashed, both published by SAMS.
 
Daniel is the developer behind several acclaimed Windows Phone apps including Surfy, Intellicam, and Splashbox; and is the creator of a number of popular open-source projects including Calcium SDK, and Clog.
 
Would you like Daniel to bring value to your organisation? Please contact

Daniel's Blog | MVP profile | Follow on Twitter
 
Windows Phone Experts
Follow on   Twitter   Google+   LinkedIn

Comments and Discussions

 
GeneralMy vote of 5 Pinmemberhisss17-Aug-12 2:28 
GeneralMy vote of 5 Pinmembermanoj kumar choubey17-Apr-12 3:05 
GeneralMy vote of 5 PinmemberEspen Harlinn5-Jan-11 10:56 
Great work
GeneralRe: My vote of 5 PinmvpDaniel Vaughan2-Jul-11 6:15 
QuestionHow do I register a custom DataService and IDataService? Pinmembercraigypdog19-Sep-10 10:07 
AnswerRe: How do I register a custom DataService and IDataService? PinmvpDaniel Vaughan20-Sep-10 1:25 
GeneralGreat project PinmemberRAbbit126-Feb-10 9:06 
GeneralRe: Great project PinmvpDaniel Vaughan6-Feb-10 10:22 
GeneralI'm just speechless... PinmemberJohn Radley4-Aug-09 13:33 
GeneralRe: I'm just speechless... PinmemberDaniel Vaughan5-Aug-09 4:01 
GeneralRe: I'm just speechless... PinmemberOption Greek18-Aug-09 2:16 
GeneralRe: I'm just speechless... PinmemberDaniel Vaughan18-Aug-09 3:31 
GeneralMy Vote of 5 Pinmembermtonsager18-Jul-09 13:59 
GeneralRe: My Vote of 5 PinmemberDaniel Vaughan18-Jul-09 14:02 
GeneralMy vote of 1 Pinmembergetter@dotnet16-Jul-09 6:34 
GeneralMore information please PinmemberDaniel Vaughan16-Jul-09 8:16 
GeneralRe: More information please PinmemberShivprasad koirala16-Jul-09 15:55 
GeneralRe: More information please PinmemberDaniel Vaughan16-Jul-09 22:55 
GeneralRe: More information please PinmemberJammer17-Jul-09 4:37 
GeneralRe: More information please PinmemberDaniel Vaughan17-Jul-09 8:17 
GeneralAnother Great Submission PinmvpAbhijit Jana15-Jul-09 3:17 
GeneralRe: Another Great Submission PinmemberDaniel Vaughan15-Jul-09 3:21 
GeneralThis ... PinmemberJammer14-Jul-09 3:54 
GeneralRe: This ... PinmemberDaniel Vaughan14-Jul-09 8:19 
GeneralReally great work Pinmemberdisore10-Jul-09 0:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web03 | 2.8.141223.1 | Last Updated 23 Nov 2009
Article Copyright 2009 by Daniel Vaughan
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid