Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / Windows Forms
Article

Clog: Client Logging, WPF Edition

Rate me:
Please Sign up or sign in to vote.
4.51/5 (68 votes)
25 Dec 2008LGPL312 min read 155K   960   114   32
A customizable log provider system that allows you to harness your existing logging system to log client side messages to your server using WCF. Includes WPF sample applications.


Clog - All your log are belong to Clog.

Contents

Introduction

My previous article, Clog - Silverlight edition, introduced a solution to the need for an integrated approach to client side logging. The solution was a fully customizable client side logging system, Clog, which allows you to harness your existing logging system to log client side messages to your server. Clog is now capable of providing logging services to any WCF or ASP.NET Web Service consumer. In the previous article I focused on a Silverlight implementation. In this, I will introduce Clog for WPF.

Clog is a log provider system, it is fully customizable, thread-safe, can serialize and log all Exception types, and allows filtering of messages both on the client and server side using custom or inbuilt filters, which so far consists of an IP Address Range Filter, a Role Membership filter, and three new filters including an Environment User Name Filter, a Machine Name Filter, and a Time Range Filter. In this release of Clog we have an extendible Log provider system, a log4net log strategy, a simple tracing log strategy, and a new Microsoft Enterprise Library Logging Application Block log strategy.

Clog's strength lies in its ability to provide centralized logging for different types of consumers. Not only does it allow us to log from a browser based application, but also from other servers in the enterprise. It allows us to leverage the features and flexibility afforded by WCF, and we can tailor security etc. by simply modifying the WCF config. Clog is not a logging system as such, in that it does not provide log sinks. What this means is that it doesn't provide any built in means to write to, for example, a log database or a flat file. Instead it allows you to harness your existing logging infrastructure; not forcing you to change the way you do things. There are plenty of logging systems out there, and no need to reinvent the wheel.

New Clog supports multiple log strategies. This means that, for example, in a smart client application we are not only able to use a ClientStrategy to send messages back to a server, but we can also write to a log file located on the client machine.

Clog Overview
Figure: Consuming Clog from different client applications.

This article will discuss how to set up Clog on both client and server using a custom config section and WCF configuration, it will touch briefly on WCF serialization, and will delve further into how Clog works, and its new features in this release.

Background

Since the last article, I have refactored much of Clog's core to enable more flexibility with filtering, and less distinction between server and client logging. The WPF implementation is more elegant, because there was no need to build secondary modules, as was the case when targeting the Silverlight and Desktop CLRs.

Clog System Overview

Clog's core component is the DanielVaughan.Logging.dll. It provides for most of the server side functionality. It is deployed on both the client-side and server-side. A WPF client application sends log requests server-side via a WCF service.

Logging component to Logging component
Figure: Logging component communicating across WCF channel.

The Logging component communicates with itself across a WCF channel. Clog then marshals the request to one or more third party logging system, as is shown in the following diagram.

Clog WPF edition overview.

Figure: WPF edition overview.

For a more in depth explanation of Clog's core implementation, please see the previous article.

Provision of Log Strategies

One of the things I find particularly elegant with the WPF edition is that we are able to use the same mechanism for the provision of log strategies on both client and server. There is no distinction made between the two environments, we simply have a different log strategy targeting the client environment. In the provided example, on the server side we have a log strategy to log entries to our third party logging system, and on the client side we have a log strategy that uses WCF to send log requests to the server. In both cases we let the log strategies do the work.

In the following diagram we see how log requests flow, from the client application, through to the existing logging infrastructure.

Filters and Log Strategies.

Figure: Log request flow through filters and log strategies.

When a log request occurs on the client, the client-side filters are evaluated, and the configuration checked to ensure that the request should proceed. If so, the request is sent to the server which, similarly, evaluates the server-side filters and sends the request on to the log strategies. In both situations, client and server-side, the log strategy becomes the end point for the log request (encapsulated by the ILogEntry instance).

Using Clog

To enable Clog for client side logging, first we configure the server based project to use Clog, then our client side project to use Clog.

Server Side Configuration

To enable Clog on the server side, add a reference to the DanielVaughan.Logging assembly. If you intend to use one of the included log strategies, either for log4net (Log4NetStrategy) or for Microsoft Enterprise Library Logging Application Block (EnterpriseLibraryStrategy), then add a reference to the DanielVaughan.Logging.Log4NetLogStrategy assembly or the DanielVaughan.Logging.EnterpriseLibraryLogStrategy assembly respectively.

XML
<section name="Clog" 
    type="DanielVaughan.Logging.Configuration.ClientLoggingConfigurationSectionHandler, DanielVaughan.Logging"/>

If you would like intellisense for the Clog config section in the Visual Studio text editor, place the Clog schema xsd, located in the Schemas directory of the download, into C:\Program Files\Microsoft Visual Studio 9.0\Xml\Schemas directory (replacing C:\Program Files with wherever Visual Studio is installed). A Visual Studio restart is required.

Next, create the Clog config section, as in the following excerpt.

XML
<!-- InternalLogLevel is used to monitor log messages originating from Clog, 
  and which are written to the console. Valid values are (from less to most restrictive): 
  All, Debug, Info, Warn, Error, Fatal, None. -->
<Clog InternalLogLevel="All" xmlns="http://danielvaughan.orpius.com/Clog/2/0/">											
  <LogStrategy Name="Simple" Type="ExampleWebsite.SimpleLogStrategy, ExampleWebsite">
    <Filter Name="IPAddressRange" Type="DanielVaughan.Logging.Filters.IPAddressRangeFilter, DanielVaughan.Logging"
      Begin="127.0.0.0" End="127.0.0.10"/>
  </LogStrategy>
  <LogStrategy Name="Log4Net" Type="DanielVaughan.Logging.LogStrategies.Log4NetStrategy, DanielVaughan.Logging.Log4NetLogStrategy">
    <Filter Name="IPAddressRange" Type="DanielVaughan.Logging.Filters.IPAddressRangeFilter, DanielVaughan.Logging"
      Begin="127.0.0.0" End="127.0.0.10"/>
    <!-- Uncomment to prevent access to those users that do now have membership of the specified roles. -->
    <!-- <Filter Name="RoleMembership" type="DanielVaughan.Logging.Filters.RoleMembershipFilter, DanielVaughan.Logging"
				Roles="Developer, Administrator" /> -->
  </LogStrategy>
</Clog>

As can be seen, we are able to associate a set of filters with each log strategy. Log entries are dispatched sequentially to each log strategy.

Create a new file in your Web project called ClogService.svc, open it and paste the following content.

XML
<%@ ServiceHost Language="C#" Debug="true" Service="DanielVaughan.Logging.ClogService" %>

The WCF service implementation and contracts are located in the DanielVaughan.Logging.dll assembly.

We must also configure WCF to expose our IClogService implementation.

XML
<!-- WCF Configuration. -->
<system.serviceModel>
  <!-- We need to specify aspNetCompatibilityEnabled="true" in order 
    to retrieve the IP address etc., of the caller in our ClogService. -->
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
  <behaviors>
    <serviceBehaviors>
      <behavior name="DanielVaughan.Logging.ClogServiceBehavior">
        <serviceMetadata httpGetEnabled="true"/>
        <serviceDebug includeExceptionDetailInFaults="true" 
          httpHelpPageEnabled="true"/>
      </behavior>
    </serviceBehaviors>
  </behaviors>
  <services>
    <service behaviorConfiguration="DanielVaughan.Logging.ClogServiceBehavior" 
      name="DanielVaughan.Logging.ClogService">
      <endpoint address="" binding="wsHttpBinding" 
        contract="DanielVaughan.Logging.IClogService"/>
      <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
    </service>
  </services>
</system.serviceModel>

Client Side Configuration

We enable Clog on the client side in the same manner as the server side. Add a reference to the DanielVaughan.Logging assembly.

XML
<section name="Clog" type="DanielVaughan.Logging.Configuration.ClientLoggingConfigurationSectionHandler, DanielVaughan.Logging"/>

Next, create the Clog config section, as in the following excerpt.

XML
<Clog InternalLogLevel="All" xmlns="http://danielvaughan.orpius.com/Clog/2/0/">
  <LogStrategy Name="Client" Type="DanielVaughan.Logging.LogStrategies.ClientStrategy, DanielVaughan.Logging">
  <!--
	Use the action attribute to invert the behaviour of a filter. 
	Action can be set to either Deny or Allow.
  -->

    <!-- Prevent users identified by Environment.UserName. -->
    <Filter Name="DenyUsers" 
      Type="DanielVaughan.Logging.Filters.EnvironmentUserFilter, DanielVaughan.Logging"
      Users="UserName1,UserName2,UserName3,UserName4" 
      Action="Deny" />

    <!-- Prevent machines identified by Environment.MachineName. -->
    <Filter Name="DenyMachines" 
      Type="DanielVaughan.Logging.Filters.MachineFilter, DanielVaughan.Logging"
      Machines="AnExampleMachineName1,AnExampleMachineName2" 
      Action="Deny" />

    <!-- Uncomment to restrict logging to business hours. -->
    <!--<Filter Name="BusinessHours" Type="DanielVaughan.Logging.Filters.TimeRangeFilter, DanielVaughan.Logging"
	Begin="09:00" End="17:30" />-->
  </LogStrategy>
</Clog> 

We also need to configure WCF, and this may be customized to suit.

XML
<system.serviceModel>
  <bindings>
    <wsHttpBinding>
      <binding name="WSHttpBinding_IClogService" closeTimeout="00:01:00"
        openTimeout="00:01:00" receiveTimeout="00:10:00" sendTimeout="00:01:00"
        bypassProxyOnLocal="false" transactionFlow="false" 
        hostNameComparisonMode="StrongWildcard"
        maxBufferPoolSize="524288" maxReceivedMessageSize="65536"
        messageEncoding="Text" textEncoding="utf-8" useDefaultWebProxy="true"
        allowCookies="false">
        <readerQuotas maxDepth="32" maxStringContentLength="8192" 
          maxArrayLength="16384"
          maxBytesPerRead="4096" maxNameTableCharCount="16384" />
        <reliableSession ordered="true" 
        inactivityTimeout="00:10:00" enabled="false" />
        <security mode="Message">
          <transport clientCredentialType="Windows" proxyCredentialType="None" realm="" />
          <message clientCredentialType="Windows" negotiateServiceCredential="true"
            algorithmSuite="Default" establishSecurityContext="true" />
        </security>
      </binding>
    </wsHttpBinding>
  </bindings>
  <client>
    <endpoint address="http://localhost:2099/ClogService.svc" binding="wsHttpBinding"
      bindingConfiguration="WSHttpBinding_IClogService" contract="DanielVaughan.Logging.IClogService"
      name="WSHttpBinding_IClogService">
    </endpoint>
  </client>
</system.serviceModel> 

Using Interfaces as Parameter Types in WCF

In the ClogService we use interfaces as the parameter types. The following excerpt shows the IClogService service contract that is exposed using WCF.

C#
/// <summary>
/// Provides Clog logging services
/// to remote clients.
/// </summary>
[ServiceContract(Namespace = OrganizationalConstants.ServiceContractNamespace)]
public interface IClogService
{
	/// <summary>
	/// Gets the configuration information
	/// for the log with specified logName.
	/// </summary>
	/// <param name="clientInfo">Information regarding
	/// the client from logging calls may be made.</param>
	/// <returns>The log configuration information.</returns>
	[OperationContract]
	[ServiceKnownType(typeof(ClientInfo))]
	[ServiceKnownType(typeof(ServerLogEntry))]
	ClientConfigurationData GetConfiguration(IClientInfo clientInfo);

	/// <summary>
	/// Writes the entry to the log 
	/// using the active <see cref="ILogStrategy"/>.
	/// Does not use One Way OperationContract, 
	/// which is incompatible with Silverlight.
	/// </summary>
	/// <param name="entryData">The entry data to be written.</param>
	[OperationContract]//(IsOneWay = true)]
	[ServiceKnownType(typeof(LogEntryData))]
	void WriteEntrySilverlight(ILogEntry entryData);

	/// <summary>
	/// Writes the entry to the log 
	/// using the active <see cref="ILogStrategy"/>.
	/// </summary>
	/// <param name="entryData">The entry data to be written.</param>
	[OperationContract(IsOneWay = true)]
	[ServiceKnownType(typeof(LogEntryData))]
	[ServiceKnownType(typeof(ServerLogEntry))]
	void WriteEntry(ILogEntry entryData);
}

We see here, that to allow WCF to deserialize to a known concrete type, in this case ClientInfo and LogEntryData, we decorate the methods with the ServiceKnownType attribute. (ThoughtShapes 2007) (MSDN 2007) Also worth noting is that, you'll notice, the WriteEntry method OperationContract attribute has the property IsOneWay set to true. This tells WCF not to issue a response for a reply message when the method is called; making asynchronous execution unnecessary for us. Sacha Barber provides a very nice rundown of the WCF service oriented attributes.

The GetConfiguration method now takes an IClientInfo instance. This allows us to evaluate filters in order to determine whether logging should be enabled for the caller. This is an enhancement from the previous version, which limited filter evaluation to knowledge of whether the call originated from a remote or local caller. The IClientInfo interface is intended merely as data container, and its internal concrete implementation ClientInfo is the base class for LogEntry types.

IClientInfo interface.
Figure: IClientInfo interface class diagram.

I originally had to make some compromises with the class hierarchy of the log entry types in order to maintain compatibility with the Silverlight edition of Clog. The internal hierarchy is deeper than I would otherwise like, and I will probably refactor it some time. We use the Facade pattern to simplify the presentation of the structure externally; IClientLogEntry and IServerLogEntry conglomerate and hide the underlying complexity.

ClientInfo hierarchy class diagram.
Figure: ClientInfo concrete implementation and inheritors' class diagram.

Unit Testing

In the past I've relied on NUnit for just about all my unit testing. I've always found white box unit testing and web applications to be unhappy bedfellows. I am pleased by the unit testing functionality found in Visual Studio 2008, especially by the ability to write white and black box tests for ASP.NET applications. I was doubly pleased to learn that Microsoft has decided to include the unit testing functionality in the Professional version of Visual Studio 2008. I mention this because the Team System flavor was required for unit testing in Visual Studio 2005.

Unit test results.
Figure: Clog Logging unit test results.

Extending Clog

Clog Provider Model

All your log are belong to Clog.

-D. Vaughan. (See derivation)

Clog uses ILogStrategy instances to send log entries to third-party logging systems. Included with Clog are three ILogStrategy implementations. If you happen to write one for a particular 3rd party logging system, I'd love to include it in the next release (with credit of course).

Integrating Clog with your existing 3rd Party Logging System

To integrate Clog with an existing logging system, implement the ILogStrategy interface, and specify the type in the provider configuration using the LogStrategy attribute, as in the following example.

XML
<LogStrategy Name="CustomStrategy" Type="YourAssembly.Strategy, YourAssembly">
<!-- Add filters here. -->
</LogStrategy>

The following excerpt shows the ILogStrategy interface. We implement this interface to support other third party logging systems.

C#
/// <summary>
/// This defines the contract for handling logging events.
/// To create a custom strategy, implement this interface
/// and define a provider for the strategy
/// in the application configuration.
/// </summary>
public interface ILogStrategy 
{
    /// <summary>
    /// Gets the log level for the log with the specified name.
    /// </summary>
    /// <param name="clientInfo">Information regarding the caller.</param>
    /// <returns>The threshold log level.</returns>
    LogLevel GetLogLevel(IClientInfo clientInfo);

    /// <summary>
    /// Logs the specified client log entry.
    /// <seealso cref="IServerLogEntry"/>
    /// </summary>
    /// <param name="logEntry">The log entry.</param>
    void Write(IClientLogEntry logEntry);

    /// <summary>
    /// Logs the specified server log entry. 
    /// <seealso cref="IServerLogEntry"/>
    /// </summary>
    /// <param name="logEntry">The log entry.</param>
    void Write(IServerLogEntry logEntry);
}

Please see the previous article for further information about extending Clog.

Filters

Clog uses server and now client side filters to determine what log entries to discard before they are sent to the active Log Strategy. Filters are evaluated when retrieving ClientConfigurationData, and on receipt of a log write request. This release includes five filters; two from the Silverlight edition, and three new ones.

  • IPAddressRangeFilter
    Restrict or allow log requests from a single or range of IP addresses.
  • RoleMembershipFilter
    Restrict or allow log requests from an ASP.NET Membership User with a specified role.
  • EnvironmentUserFilterNew
    Restrict or allow log requests from a user based on his or her Environment.UserName value.
  • MachineFilterNew
    Restrict or allow log requests from a specified set of machine names.
  • TimeRangeFilterNew
    Restrict or allow log requests during time periods.

The following class diagram shows the FilterBase class, and the filter subclass hierarchy.

Filters class diagram
Figure: Filters class diagram.

In order to change the result of a filter evaluation we use an action configuration value. The Action of a filter determines what will be done if the filter deems itself to be valid or invalid.

XML
<!-- Prevent users identified by Environment.UserName. -->
<add name="DenyUsers" 
    type="DanielVaughan.Logging.Filters.EnvironmentUserFilter, DanielVaughan.Logging"

    users="UserName1,UserName2,UserName3,UserName4" 
    action="Deny"/>

In this example we prevent a log message being sent from the client to the server (if it's a client side filter), or written to the log (if it's a server side filter), if the current user has an Environment.UserName that matches one in the users attribute. We can invert this behaviour by changing the action attribute to "Allow"; thus allowing the log request to pass through if the Environment.UserName matches one in the list.

Debug Mode

Clog's public API catches all exceptions that occur within the Log class. We don't have to be concerned that Clog may cause a fault in our application. We can, however, enable trace output and the calling of System.Diagnostics.Debug.Fail when any logging errors occur. To do this we specify the debug attribute of the ClientLogging configuration section like so:

XML
<ClientLogging defaultProvider="ExampleProvider" debug="true">
...
</ClientLogging>

Visual Studio Line Tag Support

When we are debugging an application, it is convenient to have Visual Studio jump to a source code locations for us. Thus we can avoid finding the file and location ourselves, by using a particular format strings targeting the Output window. The format looks like this:

<path to source file>(<line number>): [Message Text]

(Vickery 2007)

This format is built in to Clog's CodeLocation class. When we call its ToString() method we can jump to the location automatically. We can see an example of this in action below. Here we are tracing log entries. When the CodeLocation is written to the Output window, we can have Visual Studio take us to the location by double clicking on the text.

Screen shot of Visual Studio Line Tag Support
Figure: Visual Studio line tag support.

Points of interest

Get URL in WPF

One of the ways we can restrict logging in Clog is by using the URL of the client. We did this with ease in the Silverlight edition. To achieve this in a XAML Browser Application (XBAP), we must first check whether the application is network deployed. The following excerpt shows how the ClientStrategy detects whether it is an XBAP or a standalone WPF application, and populates the relevant log entry properties accordingly.

C#
if (ApplicationDeployment.IsNetworkDeployed)
{
    Uri launchUri = ApplicationDeployment.CurrentDeployment.ActivationUri;
    data.Url = launchUri.ToString();
    data.LogName = GetName(logEntry.CodeLocation.ClassName, launchUri.ToString());
}    
else
{
    data.LogName = logEntry.CodeLocation.ClassName;
}

Acquiring an IP Address with WCF

In order to determine the IP address of the consumer of a WCF service, we use the IncomingMessageProperties available on the server when a log request is received. (Henning 2007) (Allen 2007) It is important to note that the RemoteEndpointMessageProperty, which gives us access to the IP address, is not populated unless we specify aspNetCompatibilityEnabled in the server's WCF config.

XML
<!-- We need this to retrieve the IP address etc., of the caller in our ClogService. -->
<serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>

The following excerpt demonstrates how to obtain the IP addresses in WCF.

C#
MessageProperties properties = OperationContext.Current.IncomingMessageProperties;
RemoteEndpointMessageProperty endpoint = properties[RemoteEndpointMessageProperty.Name] 
    as RemoteEndpointMessageProperty;
return endpoint.Address; 

Future enhancements

  • Add non-intraday time-spans to TimeRangeFilter.
  • Provide more Unit Tests for core logging functionality.

Conclusion

This article discussed how to set up Clog on both client and server using the .NET provider model and WCF configuration, it touched briefly on WCF serialization, raised a caveat using the ASP.NET provider model in partial trust, and delved further into how Clog works, and its new features in this release. I intend to release Clog Ajax edition in the coming weeks.

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.

References

History

December 2007

  • First release.

January 2008

  • Added a config attribute to disable the use of ASP.NET Membership by Clog.
  • Integrated Silverlight and WPF Editions into the same download.

December 2008

  • Article updated to reflect new configuration format and features.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Engineer
Switzerland Switzerland
Daniel is a former senior engineer in Technology and Research at the Office of the CTO at Microsoft, working on next generation systems.

Previously Daniel was a nine-time Microsoft MVP and co-founder of Outcoder, a Swiss software and consulting company.

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 mobile apps including Surfy Browser for Android and Windows Phone. Daniel is the creator of a number of popular open-source projects, most notably Codon.

Would you like Daniel to bring value to your organisation? Please contact

Blog | Twitter


Xamarin Experts
Windows 10 Experts

Comments and Discussions

 
Questiondead project ? Pin
kiquenet.com15-Mar-16 23:09
professionalkiquenet.com15-Mar-16 23:09 
AnswerRe: dead project ? Pin
Daniel Vaughan16-Mar-16 2:06
Daniel Vaughan16-Mar-16 2:06 
GeneralErrors trying to log to windows event log Pin
ap1481-Feb-11 5:08
ap1481-Feb-11 5:08 
GeneralRe: Errors trying to log to windows event log Pin
Daniel Vaughan2-Feb-11 23:24
Daniel Vaughan2-Feb-11 23:24 
GeneralCongratulations for the article. I have a question ? [modified] Pin
hyilmaz3-Mar-09 5:07
hyilmaz3-Mar-09 5:07 
GeneralRe: Congratulations for the article. I have a question ? Pin
Daniel Vaughan10-Apr-09 4:24
Daniel Vaughan10-Apr-09 4:24 
GeneralDenial Very nice Pin
wasimsharp29-Dec-08 18:59
wasimsharp29-Dec-08 18:59 
GeneralVery Nice Daniel Pin
prasad0225-Dec-08 23:25
prasad0225-Dec-08 23:25 
GeneralRe: Very Nice Daniel Pin
Daniel Vaughan26-Dec-08 0:08
Daniel Vaughan26-Dec-08 0:08 
GeneralHands down one of the best article of this year.. Pin
Kant5-Dec-07 6:35
Kant5-Dec-07 6:35 
GeneralRe: Hands down one of the best article of this year.. Pin
Daniel Vaughan5-Dec-07 11:45
Daniel Vaughan5-Dec-07 11:45 
GeneralMy 5 Pin
indyfromoz4-Dec-07 15:09
indyfromoz4-Dec-07 15:09 
GeneralRe: My 5 Pin
Daniel Vaughan4-Dec-07 16:34
Daniel Vaughan4-Dec-07 16:34 
GeneralDaniel this is excellent Pin
Sacha Barber4-Dec-07 6:58
Sacha Barber4-Dec-07 6:58 
GeneralRe: Daniel this is excellent Pin
Daniel Vaughan4-Dec-07 13:26
Daniel Vaughan4-Dec-07 13:26 
GeneralRe: Daniel this is excellent Pin
Sacha Barber4-Dec-07 21:20
Sacha Barber4-Dec-07 21:20 
GeneralRatings Pin
VickyC#4-Dec-07 6:15
VickyC#4-Dec-07 6:15 
GeneralRe: Ratings Pin
Daniel Vaughan4-Dec-07 13:30
Daniel Vaughan4-Dec-07 13:30 
GeneralRe: Ratings Pin
Paul Conrad23-Dec-07 6:05
professionalPaul Conrad23-Dec-07 6:05 
GeneralRe: Ratings Pin
Daniel Vaughan23-Dec-07 23:41
Daniel Vaughan23-Dec-07 23:41 
GeneralRe: Ratings Pin
Paul Conrad24-Dec-07 4:42
professionalPaul Conrad24-Dec-07 4:42 
GeneralVery Good Article Pin
Abhijit Jana3-Dec-07 18:06
professionalAbhijit Jana3-Dec-07 18:06 
GeneralRe: Very Good Article Pin
Daniel Vaughan3-Dec-07 18:41
Daniel Vaughan3-Dec-07 18:41 
Hi Abhijit,

Thanks very much for your kind words. I really appreciate it.
I'm looking forward to reading your articles, particularly "Notes: A MSDN Websites style Custom Notes control in ASP.NET and C#", when CodeProject restores the images. Smile | :)

Daniel

<br/>
<hr align="left" color="black" size="1" width="200"><br/>
<b>Daniel Vaughan</b>
<small><a href="http://www.linkedin.com/in/danielvaughan" title="Daniel Vaughan LinkedIn Profile">LinkedIn Profile</a></small>
<small><a href="http://www.shelfspy.com/" title="Compare and Save">ShelfSpy</a></small>

GeneralRe: Very Good Article Pin
Abhijit Jana3-Dec-07 19:26
professionalAbhijit Jana3-Dec-07 19:26 
GeneralRe: Very Good Article Pin
Daniel Vaughan4-Dec-07 1:24
Daniel Vaughan4-Dec-07 1:24 

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

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