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:
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:
="1.0"
<SyntaxDefinition name="Custom Highlighting"
xmlns="http://icsharpcode.net/sharpdevelop/syntaxdefinition/2008">
<Color name="Comment" foreground="Green" />
<Color name="String" foreground="Cyan" />
-->
<RuleSet>
<Span color="Comment" begin="//" />
<Span color="Comment" multiline="true" begin="/\*" end="\*/" />
<Span color="String">
<Begin>"</Begin>
<End>"</End>
<RuleSet>
-->
<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>
-->
<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>
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:
- Create a new ViewModel, using the New button from the UI.
- Give the ViewModel a name using the textbox provided.
- Give the ViewModel a namespace using the textbox provided.
- Pick the required ViewModel type using the checkboxes provided.
- Add in all the referenced assemblies you have other
Type
s 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.
- 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.
- Add new properties for the ViewModel, assuming you have all the required property
Type
s from step 6.
- 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.
- Save the ViewModel.
- 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?
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:
- There is a MainWindow.xaml file which has a
MainWindowViewModel
- The
MainWindowViewModel
can hold an InMemoryViewModel
instance
- The
InMemoryViewModel
instance holds a PropertiesViewModel
which is used as a DataContext
for a PropertiesView
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.
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.
Where the Save Command in the InMemoryViewModel
looks like this:
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.
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");
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.
The relevant code from what happens when the OpenCommand is executed looks like this:
PesistentVM pesistentVM =
ViewModelPersistence.HydratePersistedViewModel(openFileService.FileName);
if (pesistentVM != null)
{
CurrentVM = new InMemoryViewModel();
PropertiesViewModel propertiesViewModel =
new PropertiesViewModel();
propertiesViewModel.IsCloseable = false;
CurrentVM.PropertiesVM = propertiesViewModel;
CurrentVM.ViewModelName = pesistentVM.VMName;
CurrentVM.CurrentViewModelType = pesistentVM.VMType;
CurrentVM.ViewModelNamespace = pesistentVM.VMNamespace;
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:
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));
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:
="1.0" ="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:
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:
This will then show the PropertyListPopup
popup window, which will allow you to manage the property Type
s (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.
From this window, you can do the following:
- 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.
- Remove a selected property, using the Remove button, second button down on right.
- 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
.
- Here is what the globally available App.xaml.cs property looks like that is used by all the individual ViewModel property comboboxes for their
ItemSource
s:
static PropertyChangedEventArgs propertyTypesChangeArgs =
ObservableHelper.CreateArgs<App>(x => x.PropertyTypes);
public ObservableCollection<String> PropertyTypes
{
get { return propertyTypes; }
set
{
if (propertyTypes != value)
{
propertyTypes = value;
NotifyPropertyChanged(propertyTypesChangeArgs);
}
}
}
- 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 Type
s 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:
- They allow the Code Compilation phase to work by adding the user's selected referenced assemblies as known reference assemblies to the compiler.
- They allow the inclusion of all the correct
using
statements within the generated code to be produced, by examining the Type
s'
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
{
public class SeperateAppDomainAssemblyLoader
{
#region Public Methods
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
private AppDomain BuildChildDomain(AppDomain parentDomain)
{
Evidence evidence = new Evidence(parentDomain.Evidence);
AppDomainSetup setup = parentDomain.SetupInformation;
return AppDomain.CreateDomain("DiscoveryRegion",
evidence, setup);
}
#endregion
class AssemblyLoader : MarshalByRefObject
{
#region Private/Internal Methods
[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)
{
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 Type
s in the referenced assemblies you picked. It was a conscious decision to have the user type in only those Type
s they need. I could have
imported the name of all the Type
s 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 Type
s 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:
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.
public class PesistentVM
{
#region Ctor
public PesistentVM()
{
VMProperties = new List<PesistentVMSingleProperty>();
}
#endregion
#region Public Properties
public String InheritenceVMType
{
get
{
switch (VMType)
{
case ViewModelType.Standard:
return "ViewModelBase";
case ViewModelType.Validating:
return "ValidatingViewModelBase";
case ViewModelType.ValidatingAndEditable:
return "EditableValidatingViewModelBase";
default:
return "ViewModelBase";
}
}
}
public ViewModelType VMType { get; set; }
public String VMName { get; set; }
public String VMNamespace { get; set; }
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:
- ViewModelXXXX.g.cs: A completely generated file, one part of a partial class.
- 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:
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:
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
:
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
{
public static class DynamicCompiler
{
#region Public Methods
public static Boolean ComplileCodeBlock(String code)
{
try
{
var provider = new CSharpCodeProvider(
new Dictionary<String, String>()
{ { "CompilerVersion", "v3.5" } });
CompilerParameters parameters = new CompilerParameters();
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);
foreach (FileInfo refAssFile in
((App)App.Current).ReferencedAssemblies.ToList())
parameters.ReferencedAssemblies.Add(refAssFile.FullName);
parameters.GenerateInMemory = true;
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.
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.
The Stucture of the Generated Code
As hinted earlier within the Code Compilation section, there are two parts of a single partial class:
- ViewModelXXXX.g.cs: A completely generated file, one part of a partial class.
- 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:
using System;
using System.Collections.Generic;
using System.Linq;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Windows.Data;
using System.Collections.Specialized;
using ClassLibrary1;
using Cinch;
namespace ViewModels
{
public partial class ViewModelA : Cinch.EditableValidatingViewModelBase
{
#region Data
private Cinch.DataWrapper<Person> someProp;
private Dictionary<String, Action>
autoPartPropertyCallBacks = new Dictionary<String, Action>();
#endregion
#region Public Properties
#region SomeProp
static PropertyChangedEventArgs somePropChangeArgs =
ObservableHelper.CreateArgs<ViewModelA>(x => x.SomeProp);
public Cinch.DataWrapper<Person> SomeProp
{
get { return someProp; }
private set
{
someProp = value;
NotifyPropertyChanged(somePropChangeArgs);
Action callback = null;
if (autoPartPropertyCallBacks.TryGetValue(
somePropChangeArgs.PropertyName, out callback))
{
callback();
}
}
}
#endregion
#endregion
#region EditableValidatingObject overrides
protected override void OnBeginEdit()
{
base.OnBeginEdit();
DataWrapperHelper.SetBeginEdit(cachedListOfDataWrappers);
}
protected override void OnEndEdit()
{
base.OnEndEdit();
DataWrapperHelper.SetEndEdit(cachedListOfDataWrappers);
}
protected override void OnCancelEdit()
{
base.OnCancelEdit();
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:
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;
using ClassLibrary1;
using Cinch;
namespace ViewModels
{
public partial class ViewModelA
{
#region Data
private IEnumerable<DataWrapperBase> cachedListOfDataWrappers;
private ViewMode currentViewMode = ViewMode.AddMode;
#endregion
#region Ctor
public ViewModelA()
{
#region Create DataWrappers
SomeProp = new Cinch.DataWrapper<Person>(this,somePropChangeArgs);
cachedListOfDataWrappers =
DataWrapperHelper.GetWrapperProperties<ViewModelA>(this);
#endregion
#region Create Auto Generated Property Callbacks
Action somePropCallback = new Action(SomePropChanged);
autoPartPropertyCallBacks.Add(somePropChangeArgs.PropertyName,
somePropCallback);
#endregion
}
static ViewModelA()
{
}
#endregion
#region Auto Generated Property Changed CallBacks
private void SomePropChanged()
{
}
#endregion
static PropertyChangedEventArgs currentViewModeChangeArgs =
ObservableHelper.CreateArgs<ViewModelA>(x => x.CurrentViewMode);
public ViewMode CurrentViewMode
{
get { return currentViewMode; }
set
{
currentViewMode = value;
DataWrapperHelper.SetMode(
cachedListOfDataWrappers,
currentViewMode);
NotifyPropertyChanged(currentViewModeChangeArgs);
}
}
#region Overrides
public override bool IsValid
{
get
{
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