Click here to Skip to main content
15,861,168 members
Articles / Desktop Programming / WPF

Cinch MVVM Framework Code Generator

Rate me:
Please Sign up or sign in to vote.
4.98/5 (100 votes)
5 Dec 2009CPOL21 min read 213.9K   140   74
A code generator for my Cinch MVVM Framework.

Contents

Introduction

What is this article all about? Well, some of you may already know, while others may not, that I just finished writing a series of articles about my own MVVM framework for WPF called Cinch. There are six articles in the Cinch article series, and the source code for Cinch is now hosted at CodePlex.

Here are the original Cinch articles in case you missed them, and want a read through:

You may be wondering what is left to cover. Well, in terms of Cinch itself, nothing really, it's all good. I am actually genuinely stoked with how Cinch turned out, and just how easy it makes my life, but I just felt there was room to help out even further, so I decided to create a Cinch code generator, to ease the process of creating Cinch ViewModels even further, you know, make the whole process a "cinch".

Here is a screenshot of the Cinch code generator in action:

Image 1

And here is what the text highlighting looks like, which uses the most excellent AvalonEdit control by Daniel Granwald, which is a free control available from http://www.codeproject.com/KB/edit/AvalonEdit.aspx.

This used to use the AqiStar control which is a commercially available control, which was an ace control, but people that downloaded this article could not use it, so I switched to using the free one by Daniel Grunwald, which I have to say offers the same features.

Daniel's AvalonEdit control allows custom syntax highlighting via the use of an embedded resource file called "CustomHighlighting.xshd", which looks like this:

XML
<?xml version="1.0"?>
<SyntaxDefinition name="Custom Highlighting" 
         xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
     <Color name="Comment" foreground="Green" />
     <Color name="String" foreground="Cyan" />
     
     <!-- This is the main ruleset. -->
     <RuleSet>
          <Span color="Comment" begin="//" />
          <Span color="Comment" multiline="true" begin="/\*" end="\*/" />
          
          <Span color="String">
               <Begin>"</Begin>
               <End>"</End>
               <RuleSet>
                    <!-- nested span for escape sequences -->
                    <Span begin="\\" end="." />
               </RuleSet>
          </Span>
 
    <Keywords foreground="White">
      <Word>?</Word>
      <Word>,</Word>
      <Word>.</Word>
      <Word>;</Word>
      <Word>(</Word>
      <Word>)</Word>
      <Word>[</Word>
      <Word>]</Word>
      <Word>{</Word>
      <Word>}</Word>
      <Word>+</Word>
      <Word>-</Word>
      <Word>/</Word>
      <Word>%</Word>
      <Word>*</Word>
      <Word><</Word>
      <Word>></Word>
      <Word>^</Word>
      <Word>=</Word>
      <Word>~</Word>
      <Word>!</Word>
      <Word>|</Word>
      <Word>&</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>this</Word>
      <Word>base</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>as</Word>
      <Word>is</Word>
      <Word>new</Word>
      <Word>sizeof</Word>
      <Word>typeof</Word>
      <Word>true</Word>
      <Word>false</Word>
      <Word>stackalloc</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>else</Word>
      <Word>if</Word>
      <Word>switch</Word>
      <Word>case</Word>
      <Word>default</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>do</Word>
      <Word>for</Word>
      <Word>foreach</Word>
      <Word>in</Word>
      <Word>while</Word>
    </Keywords>
 
    <Keywords foreground="CornFlowerBlue">
      <Word>break</Word>
      <Word>continue</Word>
      <Word>goto</Word>
      <Word>return</Word>
    </Keywords>
 
    <Keywords foreground="CornFlowerBlue">
      <Word>yield</Word>
      <Word>partial</Word>
      <Word>global</Word>
      <Word>where</Word>
      <Word>select</Word>
      <Word>group</Word>
      <Word>by</Word>
      <Word>into</Word>
      <Word>from</Word>
      <Word>ascending</Word>
      <Word>descending</Word>
      <Word>orderby</Word>
      <Word>let</Word>
      <Word>join</Word>
      <Word>on</Word>
      <Word>equals</Word>
      <Word>var</Word>
      <Word>dynamic</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>try</Word>
      <Word>throw</Word>
      <Word>catch</Word>
      <Word>finally</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>checked</Word>
      <Word>unchecked</Word>
    </Keywords>
 
    <Keywords foreground="CornFlowerBlue">
      <Word>fixed</Word>
      <Word>unsafe</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>bool</Word>
      <Word>byte</Word>
      <Word>char</Word>
      <Word>decimal</Word>
      <Word>double</Word>
      <Word>enum</Word>
      <Word>float</Word>
      <Word>int</Word>
      <Word>long</Word>
      <Word>sbyte</Word>
      <Word>short</Word>
      <Word>struct</Word>
      <Word>uint</Word>
      <Word>ushort</Word>
      <Word>ulong</Word>
    </Keywords>
 
    <Keywords foreground="CornFlowerBlue">
      <Word>class</Word>
      <Word>interface</Word>
      <Word>delegate</Word>
      <Word>object</Word>
      <Word>string</Word>
    </Keywords>
 
    <Keywords foreground="CornFlowerBlue">
      <Word>void</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>explicit</Word>
      <Word>implicit</Word>
      <Word>operator</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>params</Word>
      <Word>ref</Word>
      <Word>out</Word>
    </Keywords>
 
    <Keywords foreground="CornFlowerBlue">
      <Word>abstract</Word>
      <Word>const</Word>
      <Word>event</Word>
      <Word>extern</Word>
      <Word>override</Word>
      <Word>readonly</Word>
      <Word>sealed</Word>
      <Word>static</Word>
      <Word>virtual</Word>
      <Word>volatile</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>public</Word>
      <Word>protected</Word>
      <Word>private</Word>
      <Word>internal</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>namespace</Word>
      <Word>using</Word>
    </Keywords>
 
    <Keywords foreground="CornFlowerBlue">
      <Word>lock</Word>
    </Keywords>
 
    <Keywords foreground="CornFlowerBlue">
      <Word>get</Word>
      <Word>set</Word>
      <Word>add</Word>
      <Word>remove</Word>
    </Keywords>
 
    <Keywords fontWeight="bold" foreground="CornFlowerBlue">
      <Word>null</Word>
      <Word>value</Word>
    </Keywords>
          
          <!-- Digits -->
          <Rule foreground="Cyan">
            \b0[xX][0-9a-fA-F]+  # hex number
        |    \b
            (    \d+(\.[0-9]+)?   #number with optional floating point
            |    \.[0-9]+         #or just starting with floating point
            )
            ([eE][+-]?[0-9]+)? # optional exponent
        </Rule>
     </RuleSet>
</SyntaxDefinition>

Image 2

This article and the accompanying code represent the work I have done to create a Cinch code generator. The application itself was built entirely using Cinch, and it also creates Cinch ViewModel files.

Here are some of the highlights of what it does:

  • Allows user to save and re-edit a ViewModel (using XML persistence)
  • Validation to ensure the required ViewModel data is filled in by the user
  • Allows users to add their own property types
  • Allows users to include referenced assemblies, for extra types
  • Compile time checking of the generated code, to ensure code produced is of the highest possible standards; of course, the user can still choose to save a badly compiled file, if they think they know more than a compiler
  • Syntax highlighting of generated code within the actual code generator UI
  • Only produces code that is required; for example, if the user picks a Standard type ViewModel, no IsValid logic is produced in the ViewModel code
  • Partial classes which comprise of two parts:
    • xxx.g.cs: Truly generated code, which should never be edited manually
    • xxx.cs: A nice starting point to set you on your way, with the rest of your ViewModel logic, with some nice helper code already in place
  • It actually showcases Cinch itself quite nicely, and serves as a second Cinch demo app actually

There is a lot there, and I have given most of it a lot of thought, so I hope you will find it as useful as I think it will be.

Prerequisites

There are no real prerequisites as such, as the Cinch code generator has all you need.

The Cinch code generator (app for this article) actually uses the latest version of Cinch, but it may be of interest to you to see the Cinch source code for yourself, so if you are that sort of person, I would urge you to examine the Cinch source code and read the articles listed above.

Steps to Follow to Start Churning Out Code

Some of you may be the sort of people that can just pick something up and start using it without wanting to know the full details. I salute you, I can't do that, I have to dive in there and rip it to pieces straight away. If you just want to bang some code out, here are the steps you should follow to get some actual code produced using the Cinch code generator:

  1. Create a new ViewModel, using the New button from the UI.
  2. Give the ViewModel a name using the textbox provided.
  3. Give the ViewModel a namespace using the textbox provided.
  4. Pick the required ViewModel type using the checkboxes provided.
  5. Add in all the referenced assemblies you have other Types in, that you think you may need to expose as property values from the ViewModel. You can do this using the Manage Referenced Assemblies button, which will open the ReferencedAssembliesPopup window which will allow you to add/remove globally available referenced assemblies. These referenced assemblies are persisted to file so they are available for future ViewModels you create after the current UI session is closed.
  6. Add in all the property types you think you may need, by using the Manage Properties button, which will open the PropertyListPopup popup window, which will allow you to add/remove globally available properties. The clever part is that you are able to type in a Type that may be in one of the referenced assemblies that you included in step 5. That is, if you added a referenced assembly that contains the Type you want to enter as an available property Type. Say you included a PeopleLib.dll in step 5, which contained a Person type, this could not be entered as an allowable property Type until you have added PeopleLib.dll as a referenced assembly as described in step 5. All properties in the PropertyListPopup popup window are persisted to file so they are available for future ViewModels you create after the current UI session is closed.
  7. Add new properties for the ViewModel, assuming you have all the required property Types from step 6.
  8. Give each property a name and decide if you want to use DataWrapper<T> for the property in the generated code. The DataWrapper<T> objects allow the ViewModel to control the editability of the data, so I like them, but it's up to you.
  9. Save the ViewModel.
  10. Generate the ViewModel using the Generate Code button, which will then compile the code, and alert you if anything is wrong, and will give you the option of generating the code anyway if you feel you know more than a compiler.

I will be covering how all this works in quite a bit of detail below, so if you want to know more, you have come to the right place. Read on dear reader, read on.

How it All Works, For Those That Care

Reading this section is by no means mandatory, and if you want to skip it, and go straight to the voting, where you should score this article a 5 (that's a joke, by the way), that's fine.

But if you would like to know how it works and what area to look in if you want to edit something on your own, you probably should read the following subsections which explain how the Cinch code generator actually works.

Caveat: As the code generator is designed and implemented using Cinch, I will not be covering the stuff that has already been covered by past Cinch articles. I will only cover the parts that I think are important.

Structure of the App

I think the best way to explain how it all fits together is with a screenshot or two. So let's have a look at some screenshots, shall we?

Image 3

When you create your first ViewModel using the Cinch code generator and add a property or two, you will see something like the image above. What does the image tell us about how the code is structured? Well, from the image above, we can tell that there are the following attributes to the code generator code:

  1. There is a MainWindow.xaml file which has a MainWindowViewModel
  2. The MainWindowViewModel can hold an InMemoryViewModel instance
  3. The InMemoryViewModel instance holds a PropertiesViewModel which is used as a DataContext for a PropertiesView

Image 4

If we examine the image above, we can see that there are three buttons (top right, just below the Minimise/Maximise/Close window buttons) which the user can click; the inner of the three creates a new property (which is added to the ViewModel in progress), the next one (the one with the arrow in the image above) is used to open up the PropertyListPopup popup window. The popup is opened using the previously discussed (in previous Cinch articles, that is) Cinch.IUIVisualizerService service.

From the PropertyListPopup popup window, the user is also able to open up another popup window called StringEntryPopup which again is opened using the Cinch.IUIVisualizerService service. From the StringEntryPopup, the user is able to add new property types to the list of available properties.

Image 5

Using the last of the three buttons, the user is able to open up another popup called ReferencedAssembliesPopup from where the user is able to browse to any referenced assemblies that contain Types that they may need to use as exposed properties within their current code generator ViewModel that is being worked on. This will be discussed in more detail later, do not worry.

ViewModel Persistence To/From XML

One very handy part of the Cinch code generator is that it allows the persistence of a ViewModel, that may or may not be completed yet, to an XML file. This means that you can be part way through working with a ViewModel, save it to XML, and then come back and load it back up and continue working on it, or you could load an existing ViewModel that was previously saved to XML and reload it and use it as the basis for a brand new ViewModel.

To save the current ViewModel to XML, you can use the Save button.

Image 6

Where the Save Command in the InMemoryViewModel looks like this:

C#
/// <summary>
/// Executes the SaveVMCommand
/// </summary>
private void ExecuteSaveVMCommand()
{
    ClearCodeWorkSpaces();
    SaveOrGenerateOperation("Xml files (*.xml)|*.xml", 
        SaveOrGenerate.Save);
}

And this command calls the SaveOrGenerateOperation() method which I will not bore you with here. The important thing is what happens inside SaveOrGenerateOperation(), which is that the Persistence.PersistViewModel() static method is called. This code looks like the following, where it can be seen that standard XML serialization is used to persist a PersistentVM (which is a lighter weight ViewModel created from the full weight WPF ViewModel, with just the important stuff in it) to an XML file.

C#
/// <summary>
/// Serializes a PesistentVM to disk, in a XML file format
/// </summary>
/// <param name="fileName">The file name of the ViewModel to save</param>
/// <param name="vmToPersist">The actual serializable ViewModel</param>
/// <returns>True if the save operation succeeds</returns>
public static Boolean PersistViewModel(String fileName, PesistentVM vmToPersist)
{
    try
    {
        FileInfo file = new FileInfo(fileName);
        if (!file.Extension.Equals(".xml"))
            throw new NotSupportedException(
                String.Format("The file name {0} you picked is not " +
                "supported\r\n\r\nOnly .xml files are valid",
                file.Name));
 
        if (vmToPersist == null)
            throw new NotSupportedException("The ViewModel is null");
 
        
        //write the file to disk
        XmlSerializer serializer = new XmlSerializer(typeof(PesistentVM));
 
        using (TextWriter writer = new StreamWriter(file.FullName))
        {
            serializer.Serialize(writer, vmToPersist);
        }
        return true;
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

As you can imagine, there is also an OpenCommand that is used to hydrate a PersistentVM back into a full blown WPF like bindable INotifyPropertyChanged/Cinch based ViewModel.

Image 7

The relevant code from what happens when the OpenCommand is executed looks like this:

C#
//open to XML
PesistentVM pesistentVM =
    ViewModelPersistence.HydratePersistedViewModel(openFileService.FileName);
//check we got something, and recreate the full weight InMemoryViewModel from
//the lighter weight XML read PesistentVM
if (pesistentVM != null)
{
    CurrentVM = new InMemoryViewModel();
 
    //Start out with PropertiesViewModel shown
    PropertiesViewModel propertiesViewModel =
        new PropertiesViewModel();
    propertiesViewModel.IsCloseable = false;
    CurrentVM.PropertiesVM = propertiesViewModel;
    //and now read in other data
    CurrentVM.ViewModelName = pesistentVM.VMName;
    CurrentVM.CurrentViewModelType = pesistentVM.VMType;
    CurrentVM.ViewModelNamespace = pesistentVM.VMNamespace;
    //and add in the individual properties
    foreach (var prop in pesistentVM.VMProperties)
    {
        CurrentVM.PropertiesVM.PropertyVMs.Add(new
        SinglePropertyViewModel
        {
            PropertyType = prop.PropertyType,
            PropName = prop.PropName,
            UseDataWrapper = prop.UseDataWrapper
        });
    }
 
    HasContent = true;
}
else
{
    messageBoxService.ShowError(String.Format("Could not open the ViewModel {0}",
    openFileService.FileName));
}
 
}

Where the XML deserialization looks like this:

C#
/// <summary>
/// DeSerializes an XML file into a PesistentVM 
/// (if the xml is of the correct formatting)
/// </summary>
/// <param name="fileName">The file name of the ViewModel to open</param>
/// <returns>The XML read PesistentVM, or null if it can't be read</returns>
public static PesistentVM HydratePersistedViewModel(String fileName)
{
    try
    {
        FileInfo file = new FileInfo(fileName);
        if (!file.Extension.Equals(".xml"))
            throw new NotSupportedException(
                String.Format("The file name {0} you picked is not supported" +
                "\r\n\r\nOnly .xml files are valid",
                file.Name));
 
        //read the file from disk
        XmlSerializer serializer = new XmlSerializer(typeof(PesistentVM));
        serializer.UnknownNode += Serializer_UnknownNode;
        serializer.UnknownAttribute += Serializer_UnknownAttribute;
        PesistentVM vmToHydrate = null;
 
        using (FileStream fs = new FileStream(file.FullName, FileMode.Open))
        {
            vmToHydrate = (PesistentVM)serializer.Deserialize(fs);
        }
        return vmToHydrate;
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

And just in case you were wondering what the resulting ViewModel XML would look like, here is an example:

XML
<?xml version="1.0" encoding="utf-8"?>
<PesistentVM xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <VMType>ValidatingAndEditable</VMType>
  <VMName>ViewModelA</VMName>
  <VMNamespace>ViewModels</VMNamespace>
  <VMProperties>
    <PesistentVMSingleProperty>
      <PropName>count1</PropName>
      <PropertyType>Decimal</PropertyType>
      <UseDataWrapper>true</UseDataWrapper>
      <ParentViewModelName>ViewModelA</ParentViewModelName>
    </PesistentVMSingleProperty>
    <PesistentVMSingleProperty>
      <PropName>count2</PropName>
      <PropertyType>Decimal</PropertyType>
      <UseDataWrapper>true</UseDataWrapper>
      <ParentViewModelName>ViewModelA</ParentViewModelName>
    </PesistentVMSingleProperty>
  </VMProperties>
</PesistentVM>

And here is what the Cinch code generator UI may look like for this XML file:

Image 8

Property Management

One key area that you will need to cover is adding properties to your ViewModel code. The Cinch code generator allows this with a few clicks. There are really only a few steps to follow.

Click the Add Property button from the main UI window:

Image 9

This will then show the PropertyListPopup popup window, which will allow you to manage the property Types (use the Add button upper right, or you can remove using the Remove button when you have a selected item in the list) which then will be available in the combobox selections within the main UI window for each property of your ViewModel.

Image 10

From this window, you can do the following:

  1. Add a new property, using the Add button top right, which will open another popup window called StringEntryPopup which is designed to accept a single string.
  2. Image 11

  3. Remove a selected property, using the Remove button, second button down on right.
  4. OK (bottom left button) saves the changes you have made, which will create/update a file on disk, which is the current application working directory + AvailablePropertyType.txt, which will happen after you add your first properties and press OK. Pressing OK also updates a globally available static property in the App.xaml.cs class which all the SinglePropertyView available property comboboxes are using as an ItemSource.
  5. Here is what the globally available App.xaml.cs property looks like that is used by all the individual ViewModel property comboboxes for their ItemSources:
  6. C#
    /// <summary> 
    /// PropertyTypes : The list of global Property Types
    /// </summary> 
    static PropertyChangedEventArgs propertyTypesChangeArgs =
           ObservableHelper.CreateArgs<App>(x => x.PropertyTypes);
    
    public ObservableCollection<String> PropertyTypes
    {
        get { return propertyTypes; }
        set
        {
            if (propertyTypes != value)
            {
                propertyTypes = value;
               NotifyPropertyChanged(propertyTypesChangeArgs);
            }
        }
    }
  7. Cancel (second button from left) simply closes the PropertyListPopup popup window, and does not save any of the changes the user made.

For advanced users, you may choose to edit this file yourself after the file first appears, but I would do this without the code generator running, and then re-run it, as you are effectively bypassing all the logic, around the property persistence, so you should do this outside of the running Cinch code generator.

Referenced Assembly Management

As you can imagine, you may not be able to always add in the property types you would like, as they may not be simple types such as Int32/Decimal/String etc. What if you need some Type contained in a separate assembly that your app uses, exposed as a ViewModel property? This does sound like a problem, right?

Luckily, the Cinch code generator has a solution for this. What it actually does is allows the user to pick referenced assemblies that will have Types that the user wants to use within the current ViewModel code. The user just picks these using a standard OpenFileDialog from the ReferenecedAssembliesPopup popup window. These referenced assembly locations are persisted to a text file on disk so that the next time the user creates a new ViewModel or opens an existing ViewModel, all the previously selected referenced assemblies will be present.

For advanced users, this file will be the current application exe path + "ReferencedAssemblies.txt"; as before, I would edit this file outside of the Cinch code generator session and then run the Cinch code generator afterwards.

The referenced assemblies serve two purposes:

  1. They allow the Code Compilation phase to work by adding the user's selected referenced assemblies as known reference assemblies to the compiler.
  2. They allow the inclusion of all the correct using statements within the generated code to be produced, by examining the Types' namespaces contained in the referenced assemblies.

This sounds all well and good, but hang on a minute. Those of you that have used Reflection before will realise, to extract some data out of an assembly, we we need to load it, and the user could be creating loads of ViewModels with lots of reference assemblies. So where would all these assemblies get loaded by default?

The answer to that is, the current AppDomain, which did not sit well with me at all. I wanted the referenced assemblies the user picked to be loaded up using the smallest memory footprint possible, and then unloaded. This sounds like a separate AppDomain to me. Which is exactly what is done, the referenced assemblies are loaded into a separate AppDomain reflected over (ReflectionOnly loading, of course) and then, when the desired information has been gleaned, the AppDomain is unloaded.

Now, this code turned out to be a bit trickier than I first thought, and the secret lies in the use of a loader object which inherits from MarshalByRefObject.

Anyway, without further ado, here is the relevant code:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Globalization;
using System.Security.Policy;
using System.Reflection;
using System.Diagnostics.CodeAnalysis;
 
namespace CinchCodeGen
{
    /// <summary>
    /// Loads an assembly into a new AppDomain and obtains all the
    /// namespaces in the loaded Assembly, which are returned as a 
    /// List. The new AppDomain is then Unloaded.
    /// 
    /// This class creates a new instance of a 
    /// <c>AssemblyLoader</c> class
    /// which does the actual ReflectionOnly loading 
    /// of the Assembly into
    /// the new AppDomain.
    /// </summary>
    public class SeperateAppDomainAssemblyLoader
    {
        #region Public Methods
        /// <summary>
        /// Loads an assembly into a new AppDomain and obtains all the
        /// namespaces in the loaded Assembly, which are returned as a 
        /// List. The new AppDomain is then Unloaded
        /// </summary>
        /// <param name="assemblyLocation">The Assembly file 
        /// location</param>
        /// <returns>A list of found namespaces</returns>
        public List<String> LoadAssemblies(List<FileInfo> assemblyLocations)
        {
            List<String> namespaces = new List<String>();
 
            AppDomain childDomain = BuildChildDomain(
                AppDomain.CurrentDomain);
 
            try
            {
                Type loaderType = typeof(AssemblyLoader);
                if (loaderType.Assembly != null)
                {
                    AssemblyLoader loader =
                        (AssemblyLoader)childDomain.
                            CreateInstanceFrom(
                            loaderType.Assembly.Location,
                            loaderType.FullName).Unwrap();
 
                    namespaces = loader.LoadAssemblies(
                        assemblyLocations);
                }
                return namespaces;
            }
 
            finally
            {
 
                AppDomain.Unload(childDomain);
            }
        }
        #endregion
 
        #region Private Methods
        /// <summary>
        /// Creates a new AppDomain based on the parent AppDomains 
        /// Evidence and AppDomainSetup
        /// </summary>
        /// <param name="parentDomain">The parent AppDomain</param>
        /// <returns>A newly created AppDomain</returns>
        private AppDomain BuildChildDomain(AppDomain parentDomain)
        {
            Evidence evidence = new Evidence(parentDomain.Evidence);
            AppDomainSetup setup = parentDomain.SetupInformation;
            return AppDomain.CreateDomain("DiscoveryRegion",
                evidence, setup);
        }
        #endregion
 
 
        /// <summary>
        /// Remotable AssemblyLoader, this class 
        /// inherits from <c>MarshalByRefObject</c> 
        /// to allow the CLR to marshall
        /// this object by reference across 
        /// AppDomain boundaries
        /// </summary>
        class AssemblyLoader : MarshalByRefObject
        {
            #region Private/Internal Methods
            /// <summary>
            /// ReflectionOnlyLoad of single Assembly based on 
            /// the assemblyPath parameter
            /// </summary>
            /// <param name="assemblyPath">The path to the Assembly</param>
            [SuppressMessage("Microsoft.Performance",
                "CA1822:MarkMembersAsStatic")]
            internal List<String> LoadAssemblies(List<FileInfo> assemblyLocations)
            {
                List<String> namespaces = new List<String>();
                try
                {
                    foreach (FileInfo assemblyLocation in assemblyLocations)
                    {
                        Assembly.ReflectionOnlyLoadFrom(assemblyLocation.FullName);
                    }
 
                    foreach (Assembly reflectionOnlyAssembly in AppDomain.CurrentDomain.
                            ReflectionOnlyGetAssemblies())
                    {
                        foreach (Type type in reflectionOnlyAssembly.GetTypes())
                        {
                            String ns = String.Format("using {0};", type.Namespace);
                            if (!namespaces.Contains(ns))
                                namespaces.Add(ns);
                        }
                    }
                    return namespaces;
                }
                catch (FileNotFoundException)
                {
                    /* Continue loading assemblies even if an assembly
                     * can not be loaded in the new AppDomain. */
                    return namespaces;
                }
            }
            #endregion
        }
    }
}
Just because you added a referenced assembly does not mean the combo boxes that show available property types and the PropertyListPopup window will contain all the Types in the referenced assemblies you picked. It was a conscious decision to have the user type in only those Types they need. I could have imported the name of all the Types in all the referenced assemblies but I thought this a bad idea, so instead the user must still manually type in the name of the required referenced assembly Types in the PropertyListPopup / StringEntryPop popup windows, to have it appear as an available property Type to assign to a ViewModel property.

Code Compilation

The way that the code generator works is as shown in the diagram below:

Image 12

To put this into words, the user can create or modify an existing ViewModel, give it a name and namespace and some properties, and then they can click Generate (or maybe save first, probably a good idea). At the point that the Generate button is clicked, a full blown WPF ViewModel of type InMemoryViewModel is being used to bind the View to the ViewModel. So when the Generate button is clicked, this full blown WPF ViewModel is translated into something a little bit more light weight, which is known as a PersistentVM. A PersistentVM does represent all the important parts of a full blown WPF InMemoryViewModel type instance, but does not have any extra WPF baggage. It does however have extra properties which expose a bunch of nicely formatted strings, which are used by the code generation phase to create the code that will eventually be written to disk at the user chosen file location.

A PersistentVM object basically looks like this, where all the string manipulation is actually done by the individual PesistentVMSingleProperty objects. So if you really want to know how the code is created, it's all in the PesistentVMSingleProperty objects.

C#
/// <summary>
/// Represents a light weight persistable ViewModel
/// </summary>
public class PesistentVM
{
    #region Ctor
    public PesistentVM()
    {
        VMProperties = new List<PesistentVMSingleProperty>();
    }
    #endregion
 
    #region Public Properties
 
    /// <summary>
    /// ViewModel type
    /// </summary>
    public String InheritenceVMType 
    {
        get
        {
            switch (VMType)
            {
                case ViewModelType.Standard:
                    return "ViewModelBase";
                case ViewModelType.Validating:
                    return "ValidatingViewModelBase"; 
                case ViewModelType.ValidatingAndEditable:
                    return "EditableValidatingViewModelBase"; 
                default:
                    return "ViewModelBase";
            }
        }
    }
 
    /// <summary>
    /// ViewModel type
    /// </summary>
    public ViewModelType VMType { get; set; }
 
    /// <summary>
    /// Viewmodel name
    /// </summary>
    public String VMName { get; set; }
 
    /// <summary>
    /// ViewModel namespace
    /// </summary>
    public String VMNamespace { get; set; }
 
    /// <summary>
    /// Nested properties
    /// </summary>
    public List<PesistentVMSingleProperty> VMProperties { get; set; }
    #endregion
}

But before the code files are created/updated, there is something cool that happens, the code that would be written to disk is compiled to check to see if it will be valid. If it's not valid, the user may choose to ignore the warning and write the file out anyway, but these warnings more than likely should be heeded.

So how does all this work?

What Gets Compiled

The eventual aim of the Cinch code generator is to create two files:

  1. ViewModelXXXX.g.cs: A completely generated file, one part of a partial class.
  2. ViewModelXXXX.cs: A good stab at helping you get started with the second half of a partial class. You can edit this, but there will be some stuff to help you get started.

Now internally, the Cinch code generator is using the System.CodeDom.Compiler namespace to do this, but before we look into that, we need to understand something about partial classes.

Suppose you have part 1 of a Person class that looks like this:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConsoleApplication1
{
    public partial class Person
    {
        public int MyProperty { get; set; }
    }
}

And a second part of a Person class that looks like this:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ConsoleApplication1
{
    public partial class Person
    {
        public Person()
        {
            this.MyProperty = 15;
        }
    }
}

Using the System.CodeDom.Compiler namespace, we are not able to compile part 2 by itself, as we would obviously need to know about part 1 where the "MyProperty" property is actually declared, which is fair enough, no way round that. So what we need to do for the sake of compilation phase, is form a single file and pass that as a String to DynamicCompiler. And when the compilation phase succeeds or the user chooses to save the files anyway, save the two partial class strings as two separate files.

For your interest, here is the entire code for DynamicCompiler:

C#
using System.Reflection;
using System.CodeDom.Compiler;
using System.Linq;
using Microsoft.CSharp;
using System;
using System.Collections.Generic;
using System.Text;
 
using Cinch;
using System.Windows;
using System.IO;
 
namespace CinchCodeGen
{
    /// <summary>
    /// Provides a method to attempt to compile the the 
    /// generated code string.
    /// Which will verify that the generated code string 
    /// is ok to be output as a generated file from this 
    /// Cinch code generator
    /// </summary>
    public static class DynamicCompiler
    {
        #region Public Methods
        /// <summary>
        /// Validates the code string that will be generated using 
        /// the compiler services available in the 
        /// <c>System.CodeDom.Compiler</c> namespace
        /// </summary>
        /// <param name="code">The code to compile as a string</param>
        /// <returns>Boolean if the code string parameter could be 
        /// compiled using the CSharpCodeProvider</returns>
        public static Boolean ComplileCodeBlock(String code)
        {
            try
            {
                var provider = new CSharpCodeProvider(
                        new Dictionary<String, String>() 
                            { { "CompilerVersion", "v3.5" } });
 
                CompilerParameters parameters = new CompilerParameters();
 
                // Start by adding any referenced assemblies
                parameters.ReferencedAssemblies.Add("System.dll");
                parameters.ReferencedAssemblies.Add(
                    typeof(ViewModelBase).Assembly.Location);
                parameters.ReferencedAssemblies.Add(
                    typeof(System.Linq.Enumerable).Assembly.Location);
                parameters.ReferencedAssemblies.Add(
                    typeof(System.Windows.Data.CollectionViewSource).Assembly.Location);
                parameters.ReferencedAssemblies.Add(
                    typeof(System.Collections.Specialized.INotifyCollectionChanged)
                         .Assembly.Location);
                parameters.ReferencedAssemblies.Add(
                    typeof(System.Collections.Generic.List<>).Assembly.Location);
                //add in any referenced assembly locations
                foreach (FileInfo refAssFile in 
                    ((App)App.Current).ReferencedAssemblies.ToList())
                         parameters.ReferencedAssemblies.Add(refAssFile.FullName);
 
                // Load the resulting assembly into memory
                parameters.GenerateInMemory = true;
                // Now compile the whole thing 
                //Must create a fully functional assembly as the code string
                CompilerResults compiledCode =
                    provider.CompileAssemblyFromSource(parameters, code);
 
                if (compiledCode.Errors.HasErrors)
                {
                    String errorMsg = String.Empty;
                    errorMsg = compiledCode.Errors.Count.ToString() +
                        " \n Dynamically generated code threw an error. \n Errors:";
 
                    for (int x = 0; x < compiledCode.Errors.Count; x++)
                    {
                        errorMsg = errorMsg + "\r\nLine: " +
                                   compiledCode.Errors[x].Line.ToString() + " - " +
                                   compiledCode.Errors[x].ErrorText;
                    }
 
                    throw new Exception(errorMsg);
                }
                return true;
 
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
        #endregion
    }
}

One thing worth a special mention is that any reference assemblies that were picked by the user will be added as reference assemblies to the compiler, to allow the code generator to compile successfully using property types the user may have added from referenced assemblies. You can read more about this in the Referenced Assembly Management section.

What Type of Errors Can We Get

So why bother pre-compiling the code if the user can choose to save it anyway? Well, it is going to be code that could be used, so we need to make sure it is as good as possible, and besides, we may actually catch something the user just didn't think of, such as the problems shown below. There may be more, I can't think of any, but there may be more.

Bad Property Type (User mistyped a property type)

See below how I could enter Int3002. Which means I typed in Int30002 instead of Int32 in the PropertyListPopup, or in the AvailablePropertyTypes.txt file in the current app location path. Both of which we discussed in the Property Management section.

Anyway, the long and short of it is that the compiler pass will catch this before we write a crappy file to disk.

Image 13

Badly Named ViewModel/Namespace (User reserved words for ViewModel or namespace name)

See below how I could enter the text "this" which is obviously a C# reserved word for either the ViewModel name or namespace.

As before, the compiler pass will catch this before we write a crappy file to disk.

Image 14

Image 15

The Stucture of the Generated Code

As hinted earlier within the Code Compilation section, there are two parts of a single partial class:

  1. ViewModelXXXX.g.cs: A completely generated file, one part of a partial class.
  2. ViewModelXXXX.cs: A good stab at helping you get started with the second half of a partial class. You can edit this, but there will be some stuff to help you get started.

I have given a lot of thought as to what code goes into what part. There are some general rules:

ViewModelXXXX.g.cs: A completely generated file

  • If the user chose to use a DataWrapper for the property, it is declared and has its public getter/private setter in the ViewModelXXXX.g.cs part.
  • If the user chose to use an Editing ViewModel, the IEditableObject overrides will be in the ViewModelXXXX.g.cs part.
  • The ViewModelXXXX.g.cs part also creates a property callback Dictionary<String,Action> that the ViewModelXXXX.cs part can use to be notified when a property value changes in the ViewModelXXXX.g.cs part.

Here is an example of what this part of the partial class may look like:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.Collections.Specialized;
 
//Referenced assemblies
using ClassLibrary1;
 
using Cinch;
 
namespace ViewModels
{
     /// <summary>
     ///NOTE : This class was auto generated by a tool
     ///Edit this code at your peril!!!!!!
     /// </summary>
     public partial class ViewModelA : Cinch.EditableValidatingViewModelBase
     {
          #region Data
          private Cinch.DataWrapper<Person> someProp;
          //callbacks to allow auto generated part to 
          //notify custom part, when property changes
          private Dictionary<String, Action> 
               autoPartPropertyCallBacks = new Dictionary<String, Action>();
          #endregion
 
 
          #region Public Properties
          #region SomeProp
          /// <summary>
          /// SomeProp
          /// </summary>
          static PropertyChangedEventArgs somePropChangeArgs =
               ObservableHelper.CreateArgs<ViewModelA>(x => x.SomeProp);
 
          public Cinch.DataWrapper<Person> SomeProp
          {
               get { return someProp; }
               private set
               {
                    someProp = value;
                    NotifyPropertyChanged(somePropChangeArgs);
                    //Use callback to provide non auto generated 
                    //part of partial
                    //class with notification, when an auto 
                    //generated property value changes
                    Action callback = null;
                    if (autoPartPropertyCallBacks.TryGetValue(
                        somePropChangeArgs.PropertyName, out callback))
                    {
                        callback();
                    }
               }
          }
          #endregion
 
          #endregion
          #region EditableValidatingObject overrides
          /// <summary>
          /// Override hook which allows us to also put any child
          /// EditableValidatingObject objects into the BeginEdit state
          /// </summary>
          protected override void OnBeginEdit()
          {
              base.OnBeginEdit();
              //Now walk the list of properties in the ViewModel
              //and call BeginEdit() on all Cinch.DataWrapper<T>s.
              //we can use the Cinch.DataWrapperHelper class for this
              DataWrapperHelper.SetBeginEdit(cachedListOfDataWrappers);
          }
 
          /// <summary>
          /// Override hook which allows us to also put any child
          /// EditableValidatingObject objects into the EndEdit state
          /// </summary>
          protected override void OnEndEdit()
          {
              base.OnEndEdit();
              //Now walk the list of properties in the ViewModel
              //and call CancelEdit() on all Cinch.DataWrapper<T>s.
              //we can use the Cinch.DataWrapperHelper class for this
              DataWrapperHelper.SetEndEdit(cachedListOfDataWrappers);
          }
 
          /// <summary>
          /// Override hook which allows us to also put any child 
          /// EditableValidatingObject objects into the CancelEdit state
          /// </summary>
          protected override void OnCancelEdit()
          {
              base.OnCancelEdit();
              //Now walk the list of properties in the ViewModel
              //and call CancelEdit() on all Cinch.DataWrapper<T>s.
              //we can use the Cinch.DataWrapperHelper class for this
              DataWrapperHelper.SetCancelEdit(cachedListOfDataWrappers);
          }
          #endregion
     }
}

ViewModelXXXX.cs: A good stab at helping you get started

  • If the user chose to use any DataWrapper<T> for properties, an IEnumerable<DataWrapperBase> is created to use throughout both parts of the partial class. This is a cache of all the DataWrapper<T> properties the current ViewModel has, so this cached IEnumerable<DataWrapperBase> can be used quickly when needed.
  • If the user chose to use any DataWrapper<T> for properties, the actual DataWrapper<T> property setters are done within the constructor.
  • If the user chose to use any DataWrapper<T> for properties, a CurrentViewMode is provided which can be used to set the state of all the cached and contained DataWrapper<T> objects in use in the ViewModel.
  • If the user chose to use an Editing/Validating ViewModel, an example validation rule is provided, but commented out, just to show the user how to add validation rules should they want to.
  • If the user chose to use an Editing/Validating ViewModel, the IsValid override is provided.
  • The ViewModelXXXX.cs part may also create property callbacks for the Dictionary<String,Action> that the ViewModelXXXX.g.cs part declared and which are used when a property value changes in the ViewModelXXXX.g.cs part. An example callback is provided, look at the following lines:
  • C#
    Action somePropCallback = new Action(SomePropChanged);
         autoPartPropertyCallBacks.Add(somePropChangeArgs.PropertyName,
              somePropCallback);
    ...
    ...
    private void SomePropChanged()
    {
         ....
    }

Here is an example of what this part of the partial class may look like:

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.Collections.Specialized;
//Referenced assemblies
using ClassLibrary1;
 
using Cinch;
 
namespace ViewModels
{
     /// <summary>
     ///You may edit this code by hand, but there is DataWrapper code
     ///and some boiler plate code provided here, to help you on your way.
     ///A lot of which is actually quite useful, and a lot of thought has been
     ///put into, what code to place in which file parts, and this custom part
     ///does have some excellent starting code, so use it as you wish.
     ///
     ///But please note : One area that will need to be examined closely 
     /// if you decide to introduce
     ///New DataWrapper<T> properties in this part, is the IsValid override
     ///Which will need to include the dataWrappers something like:
     ///<pre>
     ///       return base.IsValid &&
     ///          DataWrapperHelper.AllValid(cachedListOfDataWrappers);
     ///</pre>
     /// </summary>
     public partial class ViewModelA
     {
          #region Data
          private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
          private ViewMode currentViewMode = ViewMode.AddMode;
          //Example rule declaration : YOU WILL NEED TO DO THIS BIT
          //private static SimpleRule quantityRule;        
          #endregion
 
          #region Ctor
          public ViewModelA()
          {
               #region Create DataWrappers
               SomeProp = new Cinch.DataWrapper<Person>(this,somePropChangeArgs);
 
               //fetch list of all DataWrappers, so they can be used 
               //again later without the need for reflection
               cachedListOfDataWrappers =
                   DataWrapperHelper.GetWrapperProperties<ViewModelA>(this);
               #endregion
 
               #region Create Auto Generated Property Callbacks
               //Create callbacks for auto generated properties in 
               //auto generated partial class part Which allows this 
               //part to know when a property in the generated part changes
               Action somePropCallback = new Action(SomePropChanged);
               autoPartPropertyCallBacks.Add(somePropChangeArgs.PropertyName,
                    somePropCallback);
 
               #endregion
 
               //    #region TODO : You WILL need to create YOUR OWN validation rules
               //    //Here is an example of how to create a validation rule
               //    //you can use this as a guide to create your own validation rules
               //    quantity.AddRule(quantityRule);
               //    #endregion
          }
        
          static ViewModelA()
          {
               //quantityRule = new SimpleRule("DataValue", "Quantity can not be < 0",
               //          (Object domainObject)=>
               //          {
               //              DataWrapper<Int32> obj = (DataWrapper<Int32>)domainObject;
               //              return obj.DataValue <= 0;
               //          });
          }        
          #endregion
 
          #region Auto Generated Property Changed CallBacks
          //Callbacks which are called whenever an auto generated property in 
          //auto generated partial class part changes
          //Which allows this part to know when a property in the generated part changes
          private void SomePropChanged()
          {
                //You can insert code here that needs to run 
                //when the SomeProp property changes
          }
 
          #endregion
          /// <summary>
          /// The current ViewMode, when changed will loop
          /// through all nested DataWrapper objects and change
          /// their state also
          /// </summary>
          static PropertyChangedEventArgs currentViewModeChangeArgs =
              ObservableHelper.CreateArgs<ViewModelA>(x => x.CurrentViewMode);
 
          public ViewMode CurrentViewMode
          {
              get { return currentViewMode; }
              set
              {
                  currentViewMode = value;
                  //Now change all the cachedListOfDataWrappers
                  //Which sets all the Cinch.DataWrapper<T>s to the correct IsEditable
                  //state based on the new ViewMode applied to the ViewModel
                  //we can use the Cinch.DataWrapperHelper class for this
                  DataWrapperHelper.SetMode(
                      cachedListOfDataWrappers,
                      currentViewMode);
 
                  NotifyPropertyChanged(currentViewModeChangeArgs);
              }
          }
 
          #region Overrides
          /// <summary>
          /// Override hook which allows us to also put any child 
          /// EditableValidatingObject objects IsValid state into
          /// a combined IsValid state for the whole ViewModel,
          /// should you need to do so
          /// </summary>
          public override bool IsValid
          {
              get
              {
                 //return base.IsValid and use DataWrapperHelper, as you are
                 //using DataWrappers
                 return base.IsValid &&
                    DataWrapperHelper.AllValid(cachedListOfDataWrappers);
              }
          }
          #endregion
     }
}

That's it, Hope You Liked it

That is actually all I wanted to say right now, but I hope from this article you can see how this code generator will help you churn out good Cinch ViewModels in a matter of minutes.

What's Coming Up From Me

A some of you may be aware, this series of articles has been going on for a while now, and it has taken a lot of work to get it where it is right now, so I am off for a well earned holiday, three weeks in lovely Thailand for me. Sawadee awesomeness, noodles, and Big Chang, here I come. However, nothing lasts forever, even holidays, so when I return, I plan on looking at the following, so you can expect some articles/blogging on these topics:

  • MEF
  • n-Route and Cinch
  • A nice 3D navigation based medical app for my wife (which will, of course, use Cinch, as will any new WPF app I do from now on)
  • Maybe even some WF 4.0

Thanks

As always, votes / comments are welcome. Hell, I'd even accept a beer or 200,000,000/hot women/fast cars/clothes and anything else you think may be cool rad and knarly, they would all be graciously accepted, god knows I need them all. This series has been some serious commitment. Still, at least it's done and dusted (for now).

I am joking of course ;-) but I do need a small break.

Revisions

  • Initial release.
  • 17/10/09: Swapped syntax highlighting to use Daniel Grunwald's AvalonEdit control
  • 05/12/09: Included code stuff (commented though) to show users the new Rule creation method

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
QuestionYou're a God Pin
#realJSOP16-Dec-14 8:27
mve#realJSOP16-Dec-14 8:27 
AnswerRe: You're a God Pin
Sacha Barber16-Dec-14 8:55
Sacha Barber16-Dec-14 8:55 
GeneralMy vote of 5 Pin
LaxmikantYadav9-May-12 14:15
LaxmikantYadav9-May-12 14:15 
GeneralRe: Pin
steveaustin19803-Nov-10 22:08
steveaustin19803-Nov-10 22:08 
GeneralRe: Pin
Sacha Barber10-Jan-11 0:18
Sacha Barber10-Jan-11 0:18 
GeneralMy vote of 5 Pin
bAnzaii21-Oct-10 1:33
bAnzaii21-Oct-10 1:33 
QuestionGenerate code from DB or SQL? Pin
Mr Delphi Developer16-Dec-09 4:14
Mr Delphi Developer16-Dec-09 4:14 
AnswerRe: Generate code from DB or SQL? Pin
Sacha Barber16-Dec-09 6:48
Sacha Barber16-Dec-09 6:48 
GeneralRe: Generate code from DB or SQL? Pin
Mr Delphi Developer16-Dec-09 14:58
Mr Delphi Developer16-Dec-09 14:58 
GeneralRe: Generate code from DB or SQL? Pin
Sacha Barber16-Dec-09 21:35
Sacha Barber16-Dec-09 21:35 
GeneralAvalon Edit Link Pin
Brady Kelly30-Nov-09 4:07
Brady Kelly30-Nov-09 4:07 
GeneralCodesnippet for cinch properties [modified] Pin
Manuvdp17-Nov-09 22:48
Manuvdp17-Nov-09 22:48 
GeneralRe: Codesnippet for cinch properties Pin
Sacha Barber17-Nov-09 23:04
Sacha Barber17-Nov-09 23:04 
GeneralRe: Codesnippet for cinch properties Pin
Sacha Barber17-Nov-09 23:06
Sacha Barber17-Nov-09 23:06 
GeneralRe: Codesnippet for cinch properties Pin
Manuvdp17-Nov-09 23:18
Manuvdp17-Nov-09 23:18 
GeneralRe: Codesnippet for cinch properties Pin
Sacha Barber17-Nov-09 23:24
Sacha Barber17-Nov-09 23:24 
GeneralRe: Codesnippet for cinch properties Pin
Manuvdp18-Nov-09 2:09
Manuvdp18-Nov-09 2:09 
GeneralRe: Codesnippet for cinch properties Pin
Sacha Barber18-Nov-09 3:13
Sacha Barber18-Nov-09 3:13 
GeneralRe: Codesnippet for cinch properties Pin
Manuvdp18-Nov-09 10:13
Manuvdp18-Nov-09 10:13 
GeneralMEF Pin
Kaosmind6-Nov-09 3:13
Kaosmind6-Nov-09 3:13 
GeneralRe: MEF Pin
Sacha Barber6-Nov-09 3:31
Sacha Barber6-Nov-09 3:31 
GeneralRe: MEF Pin
Kaosmind8-Nov-09 22:21
Kaosmind8-Nov-09 22:21 
GeneralRe: MEF Pin
Sacha Barber8-Nov-09 23:30
Sacha Barber8-Nov-09 23:30 
GeneralRe: MEF Pin
Sacha Barber7-May-10 23:19
Sacha Barber7-May-10 23:19 
GeneralYeah! Pin
Wes Aday28-Oct-09 9:55
professionalWes Aday28-Oct-09 9:55 

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.