Windows Live Photo Gallery Plugin From the Ground Up

by Brian Brewder March 01, 2010 22:00

image Recently I came across Windows Live Photo Gallery which is an easy-to-use photo management tool available for free from Microsoft. This tool has a number of features that makes it a convenient tools to use for managing photos. The feature that caught my eye, and the subject of this article, is the support for 3rd party developer plugins.

This article is intended to be a tutorial to help developers get a basic plugin built without having to try to reverse engineer the sample application that Microsoft has provided as part of their documentation. This article is not intended to be a full tutorial of how Photo Gallery plugins work. For that, please see the documentation (it looks pretty decent).

  1. Install Windows Live Photo Gallery
  2. Create a .Net class library project in Visual Studio
  3. Reference C:\Program Files\Windows Live\Photo Gallery\Microsoft.WindowsLive.PublishPlugins.dll (or wherever Photo Gallery was installed to, Program Files (x86) for 64 bit OS).
  4. Reference System.Windows.Forms.dll (in the GAC)
  5. Create a class that implements Microsoft.WindowsLive.PublishPlugins.IPublishPlugin
  6. Compile the dll
  7. Register the plugin. Copy the following text into a .reg file, update the names, then run it (or open the Registry Editor and edit the keys manually).
    Windows Registry Editor Version 5.00
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Live\PublishPlugins\My Plugin]
    "AssemblyPath"="C:\\My Path\\MyPlugin.dll"
    "ClassName"="MyNamespace.MyClass"
    "FriendlyName"="My Plugin"
    "IconPath"="C:\\My Path\\MyPlugin.dll,-32512"
    NOTE: If you are running a 64 bit OS the registry key should be [HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows Live\PublishPlugins\My Plugin]
  8. To debug, set the Start Action (project properties –> Debug) to “Start external program” and set the value to C:\Program Files\Windows Live\Photo Gallery\WLXPhotoGallery.exe (or wherever Photo Gallery was installed to, Program Files (x86) for 64 bit OS).
    You should be able to run the plugin within Photo Gallery by going to Publish -> More Services -> My Plugin.

From here you should be able to follow the documentation to make your plugin interesting. Good luck!

Tags:

BizArk Feature Spotlight – Command-line Parsing

by Brian Brewder February 20, 2010 20:39

Command-line parsing can be a tedious chore when you are building an application that accepts command-line arguments. But what if all you had to do was create a class with the properties that you want to accept? The BizArk framework offers a simple to use command-line parsing utility that allows you to do exactly that.

Command-line parsing in the BizArk framework has these key features:

  • Automatic initialization: Class properties are automatically set based on the command-line arguments.
  • Default properties: Send in a value without specifying the property name.
  • Value conversion: Uses the powerful ConvertEx class also included in BizArk to convert values to the proper type.
  • Boolean flags. Flags can be specified by simply using the argument (ex, /b for true and /b- for false) or by adding the value true/false, yes/no, etc.
  • Argument arrays. Simply add multiple values after the command-line name to set a property that is defined as an array. Ex, /x 1 2 3 will populate x with the array { 1, 2, 3 } (assuming x is defined as an array of integers).
  • Command-line aliases: A property can support multiple command-line aliases for it. For example, Help uses the alias ?.
  • Partial name recognition. You don’t need to spell out the full name or alias, just spell enough for the parser to disambiguate the property/alias from the others.
  • Supports ClickOnce: Can initialize properties even when they are specified as the query string in a URL for ClickOnce deployed applications. The command-line initialization method will detect if it is running as ClickOnce or not so your code doesn’t need to change when using it.
  • Automatically creates /? help: This includes nice formatting that takes into account the width of the console.
  • Load/Save command-line arguments to a file: This is especially useful if you have multiple large, complex sets of command-line arguments that you want to run multiple times.

So how do you use the command-line parsing? Start by creating a class that is derived from CmdLineObject. Add properties with data types that support conversion from a string using the BizArk ConvertEx class. In main, instantiate your class and call Initialize. That’s it.

If you want to validate your command-line or display help, you will need to check a couple of CmdLineObject properties. Other than that, you can just use your object as any other class.

Here’s an example class that uses it (you can also view an example in the RedwerbEntry project, Program.cs):

    static void Main(string[] args)
{
RedwerbCmdLine cmdLine = new RedwerbCmdLine();
cmdLine.Initialize();
// Validate only after checking to see if they requested help
// in order to prevent displaying errors when they request help.
if (cmdLine.Help || !cmdLine.IsValid())
{
DlgMgr.ShowCmdLineHelp(cmdLine);
return;
}
// do your stuff
}
    [CmdLineDefaultArg("SettingsPath")]
public class RedwerbCmdLine
: CmdLineObject
{
public RedwerbCmdLine()
{
SettingsPath = @"C:\Garb\Settings.rdb";
FadeInTime = 2000;
FadeOutTime = 1000;
RunTime = 5000;
}
[CmdLineArg(Alias="S", ShowInUsage=DefaultBoolean.True, Required=true)]
[Description("If false the splash screen is not displayed during startup.")]
public bool ShowSplashScreen { get; set; }
[CmdLineArg(Alias = "I")]
[Description("Determines how long it takes for the splash screen to fade in.")]
public int FadeInTime { get; set; }
[CmdLineArg(Alias = "O")]
[Description("Determines how long it takes for the splash screen to fade out.")]
public int FadeOutTime { get; set; }
[CmdLineArg(Alias = "R")]
[Description("Determines how long Redwerb will run.")]
public int RunTime { get; set; }
[CmdLineArg(Alias = "P", Usage = "Settings Path")]
[Description("Path to the settings file.")]
public string SettingsPath { get; set; }
[CmdLineArg(Alias = "M", Usage = "Message")]
[Description("Message to display.")]
public string Message { get; set; }
}

BizArk is available for download on the CodePlex website, http://bizark.codeplex.com.

BizArk Feature Spotlight - WebHelper

by Brian Brewder November 20, 2009 23:43

The WebHelper class is a new feature of the BizArk framework. It’s primary purpose is to replace System.Web.WebClient. The WebClient is a great class that makes it pretty easy to do simple web requests.

Unfortunately WebClient only supports a small set of requests such as simple gets, form value posts, single file uploads, and a few others. If you need to upload multiple files in the same request, you are out of luck. If you have a request that might take more than 100 seconds, you are out of luck. If you need to do anything special at all, you are pretty much out of luck and will have to use the HttpRequest object to perform the request.

HttpRequest allows you to do a lot more, but you are stuck implementing the different protocols for the content body. These are very specific and technical and easy to miss something, not to mention that it doesn’t support progress reporting (you have to build that yourself).

The Redwerb.BizArk.Core.Web.WebHelper class is intended to provide you full access to making requests without bothering you with all the mundane details. It includes the following features:

  • Supports no content type, multipart/form-data, and application/x-www-form-urlencoded. Will automatically pick the correct content type based on the properties you set. It can also support custom content types. Just inherit from Redwerb.BizArk.Core.Web.ContentType and implement the two required methods.
  • Can set the timeout for the request.
  • Simple multiple file upload with form values. WebClient makes you send values in the query string.
  • Supports in-memory file uploads. The file doesn’t actually have to exist on disk.
  • Handles compressed responses with a simple property set.
  • Supports progress. You can set an estimated response length to get an approximate total progress before a response is received. If not set, the request/response will be 50/50.
  • Supports asynchronous requests.
  • Event to modify the request before it is sent to the server. Provides you with full control over the request that is sent.
  • Event to process the response instead of having the WebHelper handle it.
  • The response is returned as a WebHelperResponse object. This object provides access to the result as well as status of the response. It also allows you to convert the response to another type other than a byte[] (such as string, image, etc).

To use the WebHelper, just set the url and call MakeRequest.

    var helper = new WebHelper();
helper.Url = "http://localhost:57492/Test/SimpleTest";
var response = helper.MakeRequest();
var result = response.ConvertResult<bool>();
If you want to upload a file and some form variables, that’s easy too!
    var helper = new WebHelper();
helper.Url = "http://localhost:57492/Test/UploadFileTest";
helper.FormValues.Add("test", "Hello");
helper.Files.Add(new UploadFile("file1", @"text\plain", "file1.txt", Encoding.UTF8.GetBytes("Hello World")));
helper.Files.Add(new UploadFile("file2", @"text\plain", "file2.txt", Encoding.UTF8.GetBytes("Goodbye World")));
var response = helper.MakeRequest();
var result = response.ResultToString();  

I’m sure there is still a lot of work to do on this component. Let me know if you find any problems!

I would like to give a special thanks to aspnetupload.com. WebHelper essentially started out as a copy of UploadHelper from this site (though I don’t think there is too much in common with it anymore).

BizArk is available for download on the CodePlex website, http://bizark.codeplex.com.

BizArk Feature Spotlight – Data Type Conversions

by Brian Brewder September 08, 2009 00:15

The System.Convert class in .Net is a great way to convert basic values to new and wonderful types. Unfortunately it only works on a very limited set of data types. To be precise, it only works if the type implements IConvertible and you can see what types are supported in the listing below.

public interface IConvertible
{
TypeCode GetTypeCode();
bool ToBoolean(IFormatProvider provider);
byte ToByte(IFormatProvider provider);
char ToChar(IFormatProvider provider);
DateTime ToDateTime(IFormatProvider provider);
decimal ToDecimal(IFormatProvider provider);
double ToDouble(IFormatProvider provider);
short ToInt16(IFormatProvider provider);
int ToInt32(IFormatProvider provider);
long ToInt64(IFormatProvider provider);
sbyte ToSByte(IFormatProvider provider);
float ToSingle(IFormatProvider provider);
string ToString(IFormatProvider provider);
object ToType(Type conversionType, IFormatProvider provider);
ushort ToUInt16(IFormatProvider provider);
uint ToUInt32(IFormatProvider provider);
ulong ToUInt64(IFormatProvider provider);
}

If you are looking to convert to other values or the type you are converting from does not implement IConvertible, you are out of luck.

The BizArk framework provides the Redwerb.BizArk.Core.ConvertEx class which extends the conversion capabilities of the .Net framework. ConvertEx can convert types based on the following conversion strategies:

  • TypeConverter: This is a class that you can implement to support conversions between any data types you wish to support. You can get a TypeConverter by calling TypeDescriptor.GetConverter(Type). TypeConverters are used in the binding support in WinForms as well as a few other places in the .Net framework.
  • ToXxx methods: This is a common naming convention for converting an object into another type. The most common of these is the System.Object.ToString() method.
  • Parameterized constructors:  This is a convention where a constructor for a class takes a typed parameter.
  • Parse static methods. This is a common convention for converting strings to a particular type. See Int32.Parse for an example.
  • Image to byte array and vice versa: This is a specific type of conversion to convert an image to a byte array for storage or sending to a stream.
  • To/from null: Reference types can be set to null, but value types cannot. In those cases ConvertEx supports the concept of empty values. An empty value is determined based the following criteria:
    • Reference type: null
    • Char: 0 (that is \0)
    • Static [Type].Empty property (if defined)
    • Static [Type].MinValue property (if defined)
    • Custom empty value. Register the empty value by calling ConvertEx.RegisterEmptyValue.

To use ConvertEx, simply call one of the ConvertEx methods. There are a number of helper methods to convert values to basic types such as integer, string, boolean, etc. However, you can also call the ConvertEx.ChangeType method to convert between any two types (as long as one of the types supports conversions from the other type).

The following example shows how to use the generic version of ConvertEx.ChangeType. As you can see, it is pretty straight forward.

var pt = new Point(5, 10);
var myPt = ConvertEx.ChangeType<MyPoint>(pt);

ConvertEx also provides a plugin architecture if you want to extend the types that it can convert to/from. To use it, just create a class that implements the Redwerb.BizArk.Core.Convert.IConvertStrategy interface and register it by calling Redwerb.BizArk.Core.Convert.ConvertStrategyMgr.SetStrategy (you must register it for each to/from type you want it to support).

BizArk is available for download on the CodePlex website, http://bizark.codeplex.com.

BizArk Feature Spotlight – Splash Screen

by Brian Brewder August 30, 2009 11:47

The splash screen is a common feature in many desktop applications. A splash screen is used to provide feedback to the user as the application initializes. If your application does not have a long initialization process, it does not need a splash screen, and should not have one (the splash screen can become annoying after a while, especially if it is not necessary).

If you are in need of a splash screen, the BizArk framework provides the capability to show the splash screen on a thread while you initialize your application in Main (the starting point in Windows applications). You do not need to worry about any of the threading issues, BizArk takes care of all that for you. If you are interested in learning how to display a form on a separate thread, check out the source code in SplashScreen.cs, BizArkWinForms project.

The BizArk SplashScreen class provides the following capabilities:

  • Displays a custom splash screen on a separate thread to allow for initialization on the main thread.
  • Provides a mechanism to send progress to the splash screen.
  • Can fade in and out to give a more finished, polished look.
  • Set a minimum time to show the splash screen.

The splash screen is easy to use. Just create your splash screen as a standard Form. In your programs Main method (usually in Program.cs), create a new instance of Redwerb.BizArk.WinForms.SplashScreen and send in the type for your custom splash screen. When you are ready to show your splash screen call ShowSplash and when you are done with it call HideSplash.

If you want to send status updates to the splash screen, your form will need to implement the ISplashScreen interface. The BizArk SplashScreen object will route the status to the correct thread so that you don’t need to worry about cross threading issues (problematic with WinForms).

Below is an example of calling the splash screen from the RedwerbEntry project (a test project available in the BizArk Framework download):

    SplashScreen splash = new SplashScreen(typeof(Splash));
splash.FadeInTime = 500;
splash.FadeOutTime = 500;
splash.MinShowTime = 3000;
splash.ShowSplash();
//todo: remove sleep (used for testing purposes).
splash.Status = "Sleeping";
System.Threading.Thread.Sleep(cmdLine.RunTime);
//todo: initialize the application including creating an instance of the main form.
//var frm = new MyForm();
//var frm.Initialize(); // or whatever you need to do to prepare the form before display.
splash.Status = "Hiding";
splash.HideSplash(true);
//Application.Run(frm);

BizArk is available for download on the CodePlex website, http://bizark.codeplex.com.

BizArk Feature Spotlight – String Templates

by Brian Brewder August 06, 2009 22:45

I’ve never liked that String.Format requires you to provide indexed arguments. I’m a bit anal and like my arguments to increase from left to right so that if I add a new argument to the beginning of the string I end up renumbering all the subsequent arguments (hey DevEx, perhaps you can add this to Refactor!?). 

This isn’t too big of a deal for small strings, but it can make it difficult to work with large strings with a lot of arguments, especially when the text and arguments change frequently. To get around this, I often times resort to using named parameters and String.Replace (see the example below).

var str = "{greeting} {name}!";
str = str.Replace("{greeting}", "Hello");
str = str.Replace("{name}", "World");
Debug.WriteLine(str);

Surprisingly performance doesn’t seem to be a major problem with this approach. Iterating 1,000,000 times using string.Format("Hello {0}: {1,2}", "World", i) took ~650 milliseconds vs string.Replace which took about ~900 milliseconds. This might add up to a significant difference in extreme situations, but in most cases it is negligible.

However one major drawback to string.Replace is that you lose the ability to specify formatting in the string which can be very handy, especially when you are dealing with regional formatting issues.

So in order to help with this, I added the StringTemplate class to BizArk which allows you to create a format string that combines the best of both worlds, named arguments along with specifying formatting in the string (see example below).

var template = new StringTemplate("{greeting} {name} on {date:dddd, MMMM dd, yyyy}!");
template["greeting"] = "Hello";
template["name"] = "World";
template["date"] = DateTime.Now;
Debug.WriteLine(template.ToString());

This will print out “Hello World on Thursday, August 06, 2009!”.

StringTemplate actually uses String.Format to format the string so any formatting that you can use in a String.Format argument you can use in a StringTemplate argument. The names are not case sensitive, but they must conform to the same standard as identifiers in C# (upper/lower case letters, numbers, and underscore).

I also created a generic version of StringTemplate. The generic version exposes a property that you can use to set the parameters. If the type has a default constructor, StringTemplate<T> will instantiate it for you (you can also set it if you prefer).  The name of the arguments should match with the name of the properties. It uses the TypeDescriptor class to get the properties, so you can implement ICustomTypeDescriptor if you want. This should make it fairly easy and painless to do mail-merge type functionality.

BizArk is available for download on the CodePlex website, http://bizark.codeplex.com.

BizArk is Back!

by Brian Brewder April 04, 2009 15:44

I lost the BizArk framework when I changed web hosts, but it is back now. Check out the full list of features on the BizArk page.

There are a few updates since the last release, but unfortunately I don't remember what they are (I made the updates a long time ago, but haven't gotten around to posting them). I believe that I've fixed some problems with the ConvertEx class as well as added some new functionality to it. I've also added some useful extension classes and fixed some problems with the splash screen not working quite right under certain circumstances.

Introducing the Redwerb Image Visualizer

by Brian Brewder April 03, 2009 19:40

I was in need of an image visualizer for Visual Studio today and I was dismayed that I wasn't able to find one that I could just download and install. Don't get me wrong, I found plenty of image visualizers, but they all required me to compile it myself. So I decided to go ahead and build one myself.

ImgVisScreenshot

The image visualizer includes the following features:

  • Scrolls if the image is too large
  • Zoom the image from 10% to 300%
  • Change the background of the frame from light to dark
  • Includes the following information about the image
    • Height
    • Width
    • Image type
    • Horizontal resolution
    • Vertical resolution
    • Size in memory (an approximation)
  • Quick access to features via keyboard shortcuts

 

[Download]

The source code is also available.

Getting the Identity Value when Inserting into a Table

by Brian Brewder January 17, 2009 15:04

Unfortunately Google failed me recently. I needed to get the value of an identity field in a Sql Server database after I inserted a record from an ASP.Net application. I'm sure the information is out there somewhere, but I wasn't able to find it (OK, I didn't really look that long).

The key to making this work was knowing two basic things.

  1. You can batch statements in a SqlCommand.
  2. SCOPE_IDENTITY is a T-SQL method that returns the last identity assigned within a particular scope.

Here is the code I wrote that gets the identity for a record that is just inserted and uses it in a second SQL statement. See line # 6 for the SCOPE_IDENTITY usage.

   1:  var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["Gleneagle"].ConnectionString);
   2:  conn.Open();
   3:  var trans = conn.BeginTransaction();
   4:  try
   5:  {
   6:      var cmd = new SqlCommand("INSERT INTO Business (Name, Description) VALUES (@Name, @Description); SELECT SCOPE_IDENTITY();", conn);
   7:      cmd.Transaction = trans;
   8:      cmd.Parameters.AddWithValue("@Name", txtBusinessName.Text);
   9:      cmd.Parameters.AddWithValue("@Description", txtContactInfo.Text);
  10:      var busId = Convert.ToInt32(cmd.ExecuteScalar());
  11:   
  12:      cmd = new SqlCommand("INSERT INTO BusinessRecommendation (BusinessID, Name, DisplayName, StreetAddress, Description) VALUES (@BusinessID, @Name, @DisplayName, @StreetAddress, @Description)", conn);
  13:      cmd.Transaction = trans;
  14:      cmd.Parameters.AddWithValue("@BusinessID", busId);
  15:      cmd.Parameters.AddWithValue("@Name", txtName.Text);
  16:      cmd.Parameters.AddWithValue("@DisplayName", txtDisplayName.Text);
  17:      cmd.Parameters.AddWithValue("@StreetAddress", txtAddress.Text);
  18:      cmd.Parameters.AddWithValue("@Description", htmDescription.Html);
  19:      cmd.ExecuteNonQuery();
  20:   
  21:      trans.Commit();
  22:   
  23:  }
  24:  catch
  25:  {
  26:      trans.Rollback();
  27:      throw;
  28:  }
  29:  finally
  30:  {
  31:      conn.Close();
  32:      conn.Dispose();
  33:      conn = null;
  34:  }

 

If you are wondering why line # 10 converts the value to an Integer, SCOPE_IDENTITY returns a numeric value which is translated to be a Decimal because an identity field in Sql Server is not required to be an integer.

Tags:

Searching for members by display name in Community Server

by Brian Brewder October 13, 2008 03:02

If you are looking for a tutorial on how to search for members by display name in Community Server 2008, look no further. After many hours of working on it, I have finally figured out the magic combination and I'm willing to share it with the world. This article is intended for developers with ASP.Net and SQL experience.

Step 1: Add fn_GetProfileElement to your database

This is a database function that I've previously blogged about. Just copy the code from my Getting Profile Properties from ASP.Net and Community Server post and run it in Query Analyzer. We'll be using this function in the next step.

Step 1: Update cs_vw_Users_FullUser

cs_vw_Users_FullUser is the view that is used to query for the list of users. We'll want to add the users display name to this view.

The display name is stored in the PropertyValuesString field in the aspnet_Profile table. This field can contain a lot of different pieces of data. The table contains another field called PropertyNames that is used to find the value. The users display name is stored in a field called commonName.

Here is the code to recreate the view:

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER OFF
GO
ALTER View [dbo].[cs_vw_Users_FullUser]
as
SELECT
U.AppUserToken,
U.Email,
U.ForceLogin,
U.IsAnonymous,
U.LastAction,
U.LastActivity,
U.MembershipID,
U.UserAccountStatus,
U.UserID,
U.UserName,
U.CreateDate,
UP.AllowSitePartnersToContact,
UP.AllowSiteToContact,
UP.EnableAvatar,
UP.EnableDisplayInMemberList,
UP.EnableDisplayUnreadThreadsOnly,
UP.EnableEmail,
UP.EnableHtmlEmail,
UP.EnableOnlineStatus,
UP.EnablePrivateMessages,
UP.EnableThreadTracking,
UP.EnableFavoriteSharing,
UP.IsAvatarApproved,
UP.IsIgnored,
UP.ModerationLevel,
UP.Points as UserPoints,
UP.PostRank,
UP.PostSortOrder,
UP.PropertyNames as UserPropertyNames,
UP.PropertyValues as UserPropertyValues,
UP.PublicToken,
UP.SettingsID,
UP.TimeZone,
UP.TotalPosts,
dbo.fn_GetProfileElement('commonName', AP.PropertyNames, AP.PropertyValuesString) as DisplayName
FROM
cs_Users U (nolock)
INNER JOIN cs_UserProfile UP (nolock) ON U.UserID = UP.UserID
LEFT JOIN aspnet_Profile AP (nolock) ON U.MembershipID = AP.UserID

Step 3: Create a custom SqlCommonDataProvider

We need to customize the SqlCommonDataProvider in order to change the fields the search uses. You will want to create this class in a separate project. I attempted to do this in the App_Code directory in the web project but couldn't get past this error:

Critical Error: DataProvider

The dataprovider class "CommonDataProvider" could not be loaded.

There might be some way to get past this error and keep the code in the web project, but I found that creating a separate assembly worked.

Once you've got your class, you will want to copy the code in SqlGenerator to it. Community Server didn't exactly make mods easy, they use many non-overridable methods. All the methods in SqlGenerator are static and this is the code you need to modify which is why we are copying it.

In BuildMemberQuery, look for this line...

sb.Append(" and ( UserName LIKE @SearchText OR Email LIKE @SearchText ) ");

and change it to this...

sb.Append(" and ( DisplayName LIKE @SearchText OR UserName LIKE @SearchText OR Email LIKE @SearchText ) ");

 

Now override GetUserList, copy the code from the base method, and call your new BuildMemberList instead of the SqlGenerator version.

command.Parameters.Add("@sqlPopulate", SqlDbType.NText).Value = BuildMemberQuery(query, this.databaseOwner, false);
command.Parameters.Add("@sqlPopulateCount", SqlDbType.NText).Value = BuildMemberQuery(query, this.databaseOwner, true);

Compile the assembly into the web projects bin directory.

Step 4: Update communityserver.config

Now that we've got our new CommonDataProvider, we need to tell Community Server to use it. The setting for this is in communityserver.config which is in the root of the web project. Look for CommonDataProvider and replace the node with this (use your assembly name of course)...

<add
name = "CommonDataProvider"
type = "MyNamespace.MySqlCommonDataProvider, MyCustomAssembly"
connectionStringName = "SiteSqlServer"    
databaseOwnerStringName = "SiteSqlServerOwner"
/>

You can download the project and config file for this from my downloads section.

 

Download - Custom Community Server Common Data Provider (sorry, this is no longer available)

 

Step 5: Upload and enjoy

If you've followed all the steps, you should now be able to search for users based on their common or display name. If you are having difficulty, make sure you check out the sample project in the downloads section.

Powered by BlogEngine.NET 1.6.0.0

About the author

I've been a software developer since 1999 and have been working with .Net since 2002. I love creating software, playing with productivity tools, and improving the process of software development. I hope you enjoy my blog. Please feel free to leave comments or contact me, I would love to hear from you.