![]() |
Platforms, Frameworks & Libraries »
Windows Presentation Foundation »
General
Intermediate
License: The Code Project Open License (CPOL)
Cinch MVVM Framework Code GeneratorBy Sacha BarberA code generator for my Cinch MVVM Framework |
C#, .NET (.NET3.0, .NET3.5), WPF, Architect, Dev, QA, Design
|
||||||||||||
|
Advanced Search Add to IE Search |
|
|
||||||||||||||||||||
So what is this article all about then, 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 6 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
So 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 screen shot of the Cinch code generator in action :
<CLICK IMAGE FOR BIGGER IMAGE>
And here is what the text highlighting looks like, which uses the most excellent AvalonEdit control by Daniel Granwald. Which is a free control which is 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.
Daniels AvalonEdit control allows custom syntax highlighting via the use of an embedded resource file called "CustomHighlighting.xshd", which looks like this
<?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>
<CLICK IMAGE FOR BIGGER IMAGE>
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:
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.
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.
Some of you may be the sort of people that can just pick something up and start using is without wanting to know the full details. I salute you, I can't do that, I have to dive in there and rip is 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.
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. 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 reference
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 the 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.Types from Step 6.DataWrapper<T>
for the property in the generated code. The DataWrapper<T>s
objects allow the ViewModel to control the
editability of the data, so I like them, but it's up to you.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.
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 you 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.
I think the best way to explain how it all fits together is with a screen shot or 2. So let's have a look at some screen shots shall we.
<CLICK IMAGE FOR BIGGER IMAGE>
When you create your 1st ViewModel using the Cinch code generator and add a property or 2, you will see something like the image above. So what does the image tell us about how the code is structured. Well from the image above we can tell that there are following attributes to the code generator code:
MainWindow.xaml file which has a MainWindowViewModelMainWindowViewModel can hold an InMemoryViewModel instanceInMemoryViewModel instance holds a PropertiesViewModel which is
used as a DataContext for a PropertiesView<CLICK IMAGE FOR BIGGER IMAGE>
If we examine the image above we can see that there are 3 buttons (top right,
just below the Minimise/Maximise/Close window buttons)which the
user can click, the inner of the 3 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.
<CLICK IMAGE FOR BIGGER IMAGE>
Using the last of the 3 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.
One very handy part of the Cinch code generator is that is allows the persistence of a ViewModel that may or may not be completed yet to a 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.

Where the Save Command in the InMemoryViewModel looks like this
/// <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 import thing is what happens inside the
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
/// <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 a OpenCommand that is used to hydrate a PersistentVM
back into a full blown WPF like bindable INotifyPropertyChanged/Cinch
based ViewModel.

The relevant code from what happens when the OpenCommand is executed looks like this:
//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
/// <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 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.
<CLICK IMAGE FOR BIGGER IMAGE>
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

This will then show the PropertyListPopup popup window, which will allow you
to manage the 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 be available in the ComboBox selections
within the main UI window for each property of your ViewModel.

From this window you can do the following:
StringEntryPopup which is designed to accept a
single string
App.xaml.cs class which all the SinglePropertyView available property
ComboBoxes are using as an ItemSource.
///
/// PropertyTypes : The list of global Property Types
///
static PropertyChangedEventArgs propertyTypesChangeArgs =
ObservableHelper.CreateArgs<App>(x => x.PropertyTypes);
public ObservableCollection<String> PropertyTypes
{
get { return propertyTypes; }
set
{
if (propertyTypes != value)
{
propertyTypes = value;
NotifyPropertyChanged(propertyTypesChangeArgs);
}
}
}
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.
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
etc. What if you needed 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 allow 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 2 purposes./p
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:
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.
The way that the code generator works is as shown in the diagram below:
<CLICK IMAGE FOR BIGGER IMAGE>
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.
/// <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.
The eventual aim of the Cinch code generator is to create 2 file
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:
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 2nd part of a Person class that looks like this:
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 Part1 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 the DynamicCompiler. And when the
compilation phase succeeds or the user chooses to save the files anyway, save
the 2 partial class Strings as 2 separate files.
For your interest here is the entire code for the DynamicCompiler.
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 compiler successfully using property types the user may have added from referenced assemblies. You can read more about this in the Referenced Assembly Management section.
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 as good as possible, and besides we may actually catch something the user just didn't think of, such as the following problems shown below, there may be more, I can't think of any, but there may be more.
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.

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.


As hinted as earlier within the Code Compilation section, there ARE 2 parts of a single partial class :
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
IEditableObject overriddes will be in the ViewModelXXXX.g.cs partDictionary<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:
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
DataWrapper<T> for properties, a
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.DataWrapper<T> for properties the actual
DataWrapper<T> property setters are done within the constructor.DataWrapper<T> for properties, a
CurrentViewMode is provided which can be used to set the state of all the
cached and contained DataWrapper<T> object is use in the ViewModelIsValid
override is provided.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: 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:
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 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 our good Cinch ViewModels in a matter of minutes.
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 of for a well earned holiday, 3 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:
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.
General
News
Question
Answer
Joke
Rant
Admin
Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.
|
PermaLink |
Privacy |
Terms of Use
Last Updated: 5 Dec 2009 Editor: |
Copyright 2009 by Sacha Barber Everything else Copyright © CodeProject, 1999-2010 Web18 | Advertise on the Code Project |