No Configuration Enterprise Library

From time to time I end up starting a new C# project and bumping into the same old infrastructure requirements like exception handling, logging, caching, etc.

At that point I always get back to Microsoft Enterprise Library... :)

I really like EntLib but this time I had other requirements that prevented me from having a configuration file.

So how the heck can you use EntLib without configuration? It turns out that it’s relatively easy but not so well documented, so that even Google had trouble finding me the solution. :)

I’ll leave it here also for future reference...

Scenario 1 – No Configuration Caching Block

Using the caching block with configuration is very simple. You create the configuration and then simply use the CacheFactory to create the manager. Something like this:

   1: ICacheManager productsCache = CacheFactory.GetCacheManager();
   2: productsCache.Add(product.ProductID, product)

To use it without configuration is a little harder.

First you need to use the CacheManagerFactory to create a factory passing in the configuration settings. Then you use that factory to create the manager.

The magic happens when you create your own class implementing the IConfigurationSource interface (available in Microsoft.Practices.EnterpriseLibrary.Common).

The final code is as follows:

   1: using Microsoft.Practices.EnterpriseLibrary.Caching.Configuration;
   2: using Microsoft.Practices.EnterpriseLibrary.Caching.BackingStoreImplementations;
   3:  
   4: namespace MyApplication
   5: {
   6:     internal class CacheServiceConfigurationSource : IConfigurationSource
   7:     {
   8:         #region Members
   9:  
  10:         private int expirationFrequencyInSeconds;
  11:         private int maximumElementsBeforeScavenging;
  12:  
  13:         #endregion
  14:  
  15:         #region Constructors
  16:  
  17:         private CacheServiceConfigurationSource()
  18:         {
  19:         }
  20:  
  21:         public CacheServiceConfigurationSource(int expirationFrequencyInSeconds, int maximumElementsBeforeScavenging)
  22:             : this()
  23:         {
  24:             this.expirationFrequencyInSeconds = expirationFrequencyInSeconds;
  25:             this.maximumElementsBeforeScavenging = maximumElementsBeforeScavenging;
  26:         }
  27:  
  28:         #endregion
  29:  
  30:         #region Public Methods
  31:  
  32:         public void Add(IConfigurationParameter saveParameter, string sectionName, ConfigurationSection configurationSection)
  33:         {
  34:         }
  35:  
  36:         public void AddSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler)
  37:         {
  38:         }
  39:  
  40:         public ConfigurationSection GetSection(string sectionName)
  41:         {
  42:             if (sectionName.Equals("cachingConfiguration", StringComparison.CurrentCulture))
  43:             {
  44:                 CacheManagerSettings result = new CacheManagerSettings();
  45:                 result.DefaultCacheManager = "DefaultCacheManager";
  46:  
  47:                 CacheStorageData storage = new CacheStorageData();
  48:                 storage.StorageEncryption = string.Empty;
  49:                 storage.Name = "NullStorage";
  50:                 storage.Type = typeof(NullBackingStore);
  51:                 result.BackingStores.Add(storage);
  52:  
  53:                 CacheManagerData manager = new CacheManagerData();
  54:                 manager.Name = "DefaultCacheManager";
  55:                 manager.NumberToRemoveWhenScavenging = 10;
  56:                 manager.MaximumElementsInCacheBeforeScavenging = this.maximumElementsBeforeScavenging;
  57:                 manager.ExpirationPollFrequencyInSeconds = this.expirationFrequencyInSeconds;
  58:                 manager.CacheStorage = "NullStorage";
  59:                 result.CacheManagers.Add(manager);
  60:  
  61:                 return result;
  62:             }
  63:  
  64:             return null;
  65:         }
  66:  
  67:         public void Remove(IConfigurationParameter removeParameter, string sectionName)
  68:         {
  69:         }
  70:  
  71:         public void RemoveSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler)
  72:         {
  73:         }
  74:  
  75:         #endregion
  76:     }
  77: }
  78:  
  79: using System;
  80: using System.Configuration;
  81: using System.Text;
  82: using Microsoft.Practices.EnterpriseLibrary.Caching;
  83: using Microsoft.Practices.EnterpriseLibrary.Caching.Expirations;
  84:  
  85: namespace MyApplication
  86: {
  87:     public static class CacheService
  88:     {
  89:         #region Members
  90:  
  91:         private static ICacheManager manager;
  92:  
  93:         #endregion
  94:  
  95:         #region Properties
  96:  
  97:         private static ICacheManager Manager
  98:         {
  99:             get
 100:             {
 101:                 if (manager == null)
 102:                 {
 103:                     CacheManagerFactory factory = new CacheManagerFactory(new CacheServiceConfigurationSource(Context.Current.Settings.Cache.ExpirationFrequencyInSeconds, Context.Current.Settings.Cache.MaximumElementsBeforeScavenging));
 104:                     manager = factory.CreateDefault();
 105:                 }
 106:  
 107:                 return manager;
 108:             }
 109:         }
 110:  
 111:         #endregion
 112:  
 113:         #region Public Methods
 114:  
 115:         public static void AddData<T>(string key, T value)
 116:         {
 117:             AddData<T>(key, value, Context.Current.Settings.Cache.ExpirationTimeInSeconds);
 118:         }
 119:  
 120:         internal static void AddData<T>(string key, T value, int expirationInSeconds)
 121:         {
 122:             // Expiration handler
 123:  
 124:             SlidingTime expirationHandler = new SlidingTime(new TimeSpan(0, 0, expirationInSeconds), DateTime.Now);
 125:  
 126:             // Add it
 127:  
 128:             Manager.Add(key, value, CacheItemPriority.Low, null, expirationHandler);
 129:         }
 130:  
 131:         #endregion
 132:     }
 133: }

Notice the GetSection method in CacheServiceConfigurationSource and the Manager property in the CacheService class.

Scenario 2 – No Configuration Logging Block

The logging application block is similar.

With configuration you use the Logger class.

Without configuration you must create a LogWriter using your own IConfigurationSource also.

   1: using System;
   2: using System.Configuration;
   3: using System.Text;
   4: using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
   5: using Microsoft.Practices.EnterpriseLibrary.Logging.Configuration;
   6: using Microsoft.Practices.EnterpriseLibrary.Logging.Formatters;
   7: using Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners;
   8:  
   9: namespace MyApplication
  10: {
  11:     internal class LoggingConfigurationSource : IConfigurationSource
  12:     {
  13:         #region Public Methods
  14:  
  15:         public void Add(IConfigurationParameter saveParameter, string sectionName, ConfigurationSection configurationSection)
  16:         {
  17:         }
  18:  
  19:         public void AddSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler)
  20:         {
  21:         }
  22:  
  23:         public ConfigurationSection GetSection(string sectionName)
  24:         {
  25:             if (sectionName.Equals("loggingConfiguration", StringComparison.CurrentCulture))
  26:             {
  27:                 // Logging configuration
  28:  
  29:                 LoggingSettings result = new LoggingSettings();
  30:                 result.DefaultCategory = "General";
  31:                 result.TracingEnabled = true;
  32:                 result.LogWarningWhenNoCategoriesMatch = true;
  33:  
  34:                 // Listners
  35:  
  36:                 FormattedEventLogTraceListenerData listener = new FormattedEventLogTraceListenerData();
  37:                 listener.Name = "EventLogListener";
  38:                 listener.Source = "Office Extensions";
  39:                 listener.Formatter = "TextFormatter";
  40:                 listener.Log = "MyApplication";
  41:                 listener.MachineName = string.Empty;
  42:                 listener.TraceOutputOptions = System.Diagnostics.TraceOptions.None;
  43:                 listener.Filter = System.Diagnostics.SourceLevels.All;
  44:                 listener.ListenerDataType = typeof(FormattedEventLogTraceListenerData);
  45:                 listener.Type = typeof(FormattedEventLogTraceListener);
  46:                 result.TraceListeners.Add(listener);
  47:  
  48:                 TraceListenerReferenceData listenerReference = new TraceListenerReferenceData();
  49:                 listenerReference.Name = "EventLogListener";
  50:  
  51:                 // Formatters
  52:  
  53:                 StringBuilder template = new StringBuilder();
  54:                 template.AppendLine("Timestamp: {timestamp}");
  55:                 template.AppendLine("Message: {message}");
  56:                 template.AppendLine("Category: {category}");
  57:                 template.AppendLine("Priority: {priority}");
  58:                 template.AppendLine("EventId: {eventid}");
  59:                 template.AppendLine("Severity: {severity}");
  60:                 template.AppendLine("Title:{title}");
  61:                 template.AppendLine("Machine: {machine}");
  62:                 template.AppendLine("Application Domain: {appDomain}");
  63:                 template.AppendLine("Process Id: {processId}");
  64:                 template.AppendLine("Process Name: {processName}");
  65:                 template.AppendLine("Win32 Thread Id: {win32ThreadId}");
  66:                 template.AppendLine("Thread Name: {threadName}");
  67:                 template.AppendLine("Extended Properties: {dictionary({key} - {value})}");
  68:  
  69:                 TextFormatterData formatter = new TextFormatterData();
  70:                 formatter.Name = "TextFormatter";
  71:                 formatter.Type = typeof(TextFormatter);
  72:                 formatter.Template = template.ToString();
  73:                 result.Formatters.Add(formatter);
  74:  
  75:                 // Category Sources
  76:  
  77:                 TraceSourceData exceptionsSource = new TraceSourceData();
  78:                 exceptionsSource.TraceListeners.Add(listenerReference);
  79:                 exceptionsSource.Name = "Exceptions";
  80:                 exceptionsSource.DefaultLevel = System.Diagnostics.SourceLevels.All;
  81:                 result.TraceSources.Add(exceptionsSource);
  82:  
  83:                 TraceSourceData traceSource = new TraceSourceData();
  84:                 traceSource.TraceListeners.Add(listenerReference);
  85:                 traceSource.Name = "Trace";
  86:                 traceSource.DefaultLevel = System.Diagnostics.SourceLevels.All;
  87:                 result.TraceSources.Add(traceSource);
  88:  
  89:                 TraceSourceData generalSource = new TraceSourceData();
  90:                 generalSource.TraceListeners.Add(listenerReference);
  91:                 generalSource.Name = "General";
  92:                 generalSource.DefaultLevel = System.Diagnostics.SourceLevels.All;
  93:                 result.TraceSources.Add(generalSource);
  94:  
  95:                 // Special Sources
  96:  
  97:                 result.SpecialTraceSources.AllEventsTraceSource.Name = "AllEvents";
  98:                 result.SpecialTraceSources.AllEventsTraceSource.DefaultLevel = System.Diagnostics.SourceLevels.All;
  99:  
 100:                 result.SpecialTraceSources.NotProcessedTraceSource.Name = "NotProcessed";
 101:                 result.SpecialTraceSources.NotProcessedTraceSource.DefaultLevel = System.Diagnostics.SourceLevels.All;
 102:  
 103:                 result.SpecialTraceSources.ErrorsTraceSource.Name = "LoggingErrorsAndWarnings";
 104:                 result.SpecialTraceSources.ErrorsTraceSource.DefaultLevel = System.Diagnostics.SourceLevels.All;
 105:                 result.SpecialTraceSources.ErrorsTraceSource.TraceListeners.Clear();
 106:                 result.SpecialTraceSources.ErrorsTraceSource.TraceListeners.Add(listenerReference);
 107:  
 108:                 // Return
 109:  
 110:                 return result;
 111:             }
 112:  
 113:             return null;
 114:         }
 115:  
 116:         public void Remove(IConfigurationParameter removeParameter, string sectionName)
 117:         {
 118:         }
 119:  
 120:         public void RemoveSectionChangeHandler(string sectionName, ConfigurationChangedEventHandler handler)
 121:         {
 122:         }
 123:  
 124:         #endregion
 125:     }
 126: }
 127:  
 128: using System;
 129: using System.Diagnostics;
 130: using System.Globalization;
 131: using Microsoft.Practices.EnterpriseLibrary.Logging;
 132:  
 133: namespace MyApplication
 134: {
 135:     public static class LoggingHandler
 136:     {
 137:         #region Constants
 138:  
 139:         private const string CategoryExceptions = "Exceptions";
 140:         private const int PriorityExceptions = 1000;
 141:         private const int EventIdExceptions = 1000;
 142:         private const string TitleExceptions = "Errors Log";
 143:         private const TraceEventType SeverityExceptions = TraceEventType.Error;
 144:  
 145:         #endregion
 146:  
 147:         #region Members
 148:  
 149:         private static LogWriter writer;
 150:  
 151:         #endregion
 152:  
 153:         #region Properties
 154:  
 155:         internal static LogWriter Writer
 156:         {
 157:             get
 158:             {
 159:                 if (writer == null)
 160:                 {
 161:                     writer = CreateWriter();
 162:                 }
 163:  
 164:                 return writer;
 165:             }
 166:         }
 167:  
 168:         #endregion
 169:  
 170:         #region Public Methods
 171:  
 172:         public static void Exception(Exception ex)
 173:         {
 174:             // Validation
 175:  
 176:             if (ex == null)
 177:             {
 178:                 throw new ArgumentNullException();
 179:             }
 180:  
 181:             // Log
 182:  
 183:             WriteEntry(CategoryExceptions, TitleExceptions, ex.ToString(), PriorityExceptions, EventIdExceptions, SeverityExceptions);
 184:         }
 185:  
 186:         #endregion
 187:  
 188:         #region Private Methods
 189:  
 190:         private static void WriteEntry(string category, string title, string message, int priority, int eventId, TraceEventType severity)
 191:         {
 192:             LogEntry entry = new LogEntry();
 193:             entry.Categories.Add(category);
 194:             entry.TimeStamp = DateTime.Now;
 195:             entry.EventId = eventId;
 196:             entry.Message = message;
 197:             entry.Priority = priority;
 198:             entry.Severity = severity;
 199:             entry.Title = title;
 200:  
 201:             Writer.Write(entry);
 202:         }
 203:  
 204:         private static LogWriter CreateWriter()
 205:         {
 206:             LogWriterFactory factory = new LogWriterFactory(new LoggingConfigurationSource());
 207:             return factory.Create();
 208:         }
 209:  
 210:         #endregion
 211:     }
 212: }

I hope this helps someone in the future.

It turns out that the greater strength of EntLib – configuration – is also, at least in my opinion, its greater weakness. Just think of how many “real-world applications” would you want to have the user messing with the configuration file to setup event viewer listeners and things like that. :)

DSL Tools #14 - Double-click in the Model Explorer

Every DSL Tools project generates this handy model explorer where you can browse the domain classes that are on your domain model.

Something like this:

Model Explorer Preview

This is particularly helpful for example to change the properties of a particular domain class. You browse for in the explorer, click on it, and simply edit the properties.

Now there is a fundamental feature that the guys at Microsoft missed in the explorer. The ability to double click a domain class there and have the domain model view show that domain class centered. A feature very useful if you have a model with a few dozens of domains classes (which is very typical).

So I put my mind to implement such behavior on my various DSL explorers. It turns out that it isn't very difficult.

1. Determine the explorer class name

The model explorer class is generated as part of your DSLPackage project. The name of the class depends on the name of your DSL. To find out the correct name, browse the DSLPackage project, under GeneratedCode, and you'll find a file named ModelExplorer.cs. The name of the explorer is the name of the first class on that file.

In my example that is UserInterfaceModelExplorer:

   1: /// <summary>
   2: /// Double-derived class to allow easier code customization.
   3: /// </summary>
   4: internal partial class UserInterfaceModelExplorer : UserInterfaceModelExplorerBase
   5: {
   6:     /// <summary>
   7:     /// Constructs a new UserInterfaceModelExplorer.
   8:     /// </summary>
   9:     public UserInterfaceModelExplorer(global::System.IServiceProvider serviceProvider)
  10:         : base(serviceProvider)
  11:     {
  12:     }
  13: }

2. Create a custom class with that same name

Under the DSLPackage, you will need to create a new custom partial class with the name of your model explorer. All the magic will happen here.

3. Override the CreateElementVisitor method

Here you should subscribe to the ObjectModelBrowser.DoubleClick event:

   1: /// <summary>
   2: /// Executed when the model explorer creates the tree element visitor.
   3: /// </summary>
   4: /// <returns>The tree element visitor.</returns>
   5: protected override IElementVisitor CreateElementVisitor()
   6: {
   7:     // Subscribe double click in the tree view
   8:  
   9:     this.ObjectModelBrowser.DoubleClick += new EventHandler(this.ObjectModelBrowser_DoubleClick);
  10:  
  11:     // Default behavior
  12:  
  13:     return base.CreateElementVisitor();
  14: }

4. Implement the corresponding event handler

The magic consists in:

  • First, finding the element that is selected in the explorer,
  • Second, getting the corresponding model element,
  • And, finally, selecting that shape in the model viewer.

Check the full source code bellow for the implementation of these steps.

5. Transform All templates

Before you test the new behavior of the model explorer, you'll need to transform all the templates to let the DSL know that you have customized your model explorer.

And that's it. Double-click any domain class in the explorer and the model viewer will be updated accordingly.

Full Source Code

   1: using System;
   2: using System.Collections.ObjectModel;
   3: using Microsoft.VisualStudio.Modeling;
   4: using Microsoft.VisualStudio.Modeling.Diagrams;
   5: using Microsoft.VisualStudio.Modeling.Shell;
   6:  
   7: namespace MyDSL.UserInterface
   8: {
   9:     /// <summary>
  10:     /// Custom behavior for UserInterfaceModelExplorer.
  11:     /// </summary>
  12:     internal partial class UserInterfaceModelExplorer
  13:     {
  14:         #region Public Methods
  15:  
  16:         /// <summary>
  17:         /// Executed when the model explorer creates the tree element visitor.
  18:         /// </summary>
  19:         /// <returns>The tree element visitor.</returns>
  20:         protected override IElementVisitor CreateElementVisitor()
  21:         {
  22:             // Subscribe double click in the tree view
  23:  
  24:             this.ObjectModelBrowser.DoubleClick += new EventHandler(this.ObjectModelBrowser_DoubleClick);
  25:  
  26:             // Default behavior
  27:  
  28:             return base.CreateElementVisitor();
  29:         }
  30:  
  31:         #endregion
  32:  
  33:         #region Private Methods
  34:  
  35:         /// <summary>
  36:         /// Selects the specified shape using the given modeling document data.
  37:         /// </summary>
  38:         /// <param name="shapeElement">The shape element that should be selected.</param>
  39:         /// <param name="docData">The modeling document data.</param>
  40:         private static void SelectShape(ShapeElement shapeElement, DocData docData)
  41:         {
  42:             // Validation
  43:  
  44:             if (shapeElement == null)
  45:             {
  46:                 throw new ArgumentNullException("shapeElement");
  47:             }
  48:  
  49:             if (docData == null)
  50:             {
  51:                 throw new ArgumentNullException("docData");
  52:             }
  53:  
  54:             // Select the shape
  55:  
  56:             ModelingDocView docView = docData.DocViews[0];
  57:             if (docView != null)
  58:             {
  59:                 docView.SelectObjects(1, new object[] { shapeElement }, 0);
  60:             }
  61:         }
  62:         
  63:         /// <summary>
  64:         /// Gets the first shape the represents the specified model element.
  65:         /// </summary>
  66:         /// <param name="modelElement">The model element whose shape will be returned.</param>
  67:         /// <returns>The first shape the represents the specified model element.</returns>
  68:         private static ShapeElement GetModelElementFirstShape(ModelElement modelElement)
  69:         {
  70:             // Presentation elements
  71:  
  72:             LinkedElementCollection<PresentationElement> presentations = PresentationViewsSubject.GetPresentation(modelElement);
  73:             foreach (ModelElement element in presentations)
  74:             {
  75:                 ShapeElement shapeElement = (element as ShapeElement);
  76:                 if (shapeElement != null)
  77:                 {
  78:                     return shapeElement;
  79:                 }
  80:             }
  81:  
  82:             // Default result
  83:  
  84:             return null;
  85:         }
  86:  
  87:         /// <summary>
  88:         /// Return the model element which is the parent of the specified model element considering that
  89:         /// this element is placed in a compartment.
  90:         /// </summary>
  91:         /// <param name="modelElement">The model element whose parent will be returned.</param>
  92:         /// <returns>
  93:         /// The model element which is the parent of the specified model element considering that
  94:         /// this element is placed in a compartment.
  95:         /// </returns>
  96:         private static ModelElement GetCompartmentElementFirstParent(ModelElement modelElement)
  97:         {
  98:             // Get the domain class associated with model element.
  99:  
 100:             DomainClassInfo domainClass = modelElement.GetDomainClass();
 101:             if (domainClass != null)
 102:             {
 103:                 // A element is only considered to be in a compartment if it participates in only 1 embedding relationship
 104:                 // This might be wrong for some models
 105:  
 106:                 if (domainClass.AllEmbeddedByDomainRoles.Count == 1)
 107:                 {
 108:                     // Get a collection of all the links to this model element
 109:                     // Since this is in a compartment there will only be one
 110:  
 111:                     ReadOnlyCollection<ElementLink> links = DomainRoleInfo.GetAllElementLinks(modelElement);
 112:                     if (links.Count == 1)
 113:                     {
 114:                         // Get the model element participating in the link that isn't the current one
 115:                         // That will be the parent
 116:                         // Probably there is a better way to achieve the same result
 117:  
 118:                         foreach (ModelElement linkedElement in links[0].LinkedElements)
 119:                         {
 120:                             if (!modelElement.Equals(linkedElement))
 121:                             {
 122:                                 return linkedElement;
 123:                             }
 124:                         }
 125:                     }
 126:                 }
 127:             }
 128:  
 129:             // Default result
 130:  
 131:             return null;
 132:         }
 133:  
 134:         #endregion
 135:  
 136:         #region Event Handlers
 137:  
 138:         private void ObjectModelBrowser_DoubleClick(object sender, EventArgs e)
 139:         {
 140:             // Get the node selected in the model explorer tree
 141:  
 142:             ModelElementTreeNode node = (this.ObjectModelBrowser.SelectedNode as ModelElementTreeNode);
 143:             if (node != null)
 144:             {
 145:                 // Get the corresponding model element
 146:  
 147:                 ModelElement element = node.ModelElement;
 148:                 if (element != null)
 149:                 {
 150:                     // Get the corresponding shape
 151:                     // If the model element is in a compartment the result will be null
 152:  
 153:                     ShapeElement shape = GetModelElementFirstShape(element);
 154:                     if (shape == null)
 155:                     {
 156:                         // If the element is in a compartment, try to get the parent model element to select that
 157:  
 158:                         ModelElement parentElement = GetCompartmentElementFirstParent(element);
 159:                         if (parentElement != null)
 160:                         {
 161:                             // Get the corresponding shape
 162:  
 163:                             shape = GetModelElementFirstShape(parentElement);
 164:                         }
 165:                     }
 166:  
 167:                     // Select the shape
 168:  
 169:                     if (shape != null)
 170:                     {
 171:                         SelectShape(shape, this.ModelingDocData);
 172:                     }
 173:                 }
 174:             }
 175:         }
 176:  
 177:         #endregion
 178:     }
 179: }

Special thanks are in order to Oleg Sych for his fundamental tips to get this working.

Posted 16 December 08 04:22 by hgr | 1 Comments   
Filed under ,
Agile Prototyping

One of these days I went for a periodic review meeting of the project I'm working on. We were going to discuss the implementation of a critical piece for our next major milestone.

We invited our User Experience Specialist and he was going to present us the first UI prototypes.

I was expecting some great WPF "almost-ready" screens with great user experience stuff and a wonderful look-and-feel like all the other prototypes they did before...

This was what I got to see:

Now talk about agile prototyping. :)

Posted 11 December 08 11:28 by hgr | 1 Comments   
Filed under
DSL Tools #13 - Changing the Appearance of a Shape at Runtime

Finally I found some time to document this simple customization to the DSL Tools designers.

The requirement is as follows:

  • I have a given domain class - called Module Reference - that has a property called Main.
  • This domain property indicates if that particular class is the "most important" of that kind in the model.
  • This domain class as an association with another domain class - called Entity. That association allows me to connect multiple entities to any given module reference.
  • All the entities connected to the main module reference need to presented with a different color (different from the default shape color defined in the DSL definition model).

The domain model is the following:

Achieving this is quite simple:

1. Create a change rule to respond to the change in the Main property of the ModuleReference domain class:

   1: using System;
   2: using Microsoft.VisualStudio.Modeling;
   3: using Microsoft.VisualStudio.Modeling.Diagrams;
   4:  
   5: namespace MyModel
   6: {
   7:     /// <summary>
   8:     /// Defines a change rule for the ModuleReference domain class.
   9:     /// </summary>
  10:     [RuleOn(typeof(Primavera.Athena.Models.Reporting.ModuleReference), FireTime = TimeToFire.TopLevelCommit)]
  11:     public class ModuleReferenceChangeRule : ChangeRule
  12:     {
  13:         #region Public Methods
  14:  
  15:         /// <summary>
  16:         /// Alerts listeners that a property for an element has changed.
  17:         /// </summary>
  18:         /// <param name="e">Provides data for the ElementPropertyChanged event.</param>
  19:         public override void ElementPropertyChanged(ElementPropertyChangedEventArgs e)
  20:         {
  21:             // Validations
  22:  
  23:             if (e == null)
  24:             {
  25:                 throw new ArgumentNullException("e");
  26:             }
  27:  
  28:             if (e.DomainProperty.Id.Equals(ModuleReference.MainDomainPropertyId))
  29:             {
  30:                 HandleMainChanged(e);
  31:             }
  32:  
  33:             // Default behavior
  34:  
  35:             base.ElementPropertyChanged(e);
  36:         }
  37:         
  38:         #endregion
  39:  
  40:         #region Private Methods
  41:  
  42:         /// <summary>
  43:         /// Handles changes in Main.
  44:         /// </summary>
  45:         /// <param name="e">The <see cref="Microsoft.VisualStudio.Modeling.ElementPropertyChangedEventArgs"/> instance containing the event data.</param>
  46:         [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1822:MarkMembersAsStatic")]
  47:         private void HandleMainChanged(ElementPropertyChangedEventArgs e)
  48:         {
  49:             // Something changed?
  50:  
  51:             if (!e.OldValue.Equals(e.NewValue))
  52:             {
  53:                 // Get the current shape
  54:  
  55:                 ModuleReference moduleReference = e.ModelElement as ModuleReference;
  56:                 if (moduleReference != null)
  57:                 {
  58:                     // Update all entities colors
  59:  
  60:                     foreach (Entity entity in moduleReference.Entities)
  61:                     {
  62:                         LinkedElementCollection<PresentationElement> presentations =
  63:                             PresentationViewsSubject.GetPresentation(entity);
  64:                         EntityShape entityShape = null;
  65:  
  66:                         if (presentations.Count > 0)
  67:                         {
  68:                             entityShape = presentations[0] as EntityShape;
  69:                         }
  70:  
  71:                         if (entityShape != null)
  72:                         {
  73:                             entityShape.UpdateMainModuleReferencePresentation((bool)e.NewValue);
  74:                         }
  75:                     }
  76:                 }
  77:             }
  78:         }
  79:  
  80:         #endregion
  81:     }
  82: }

Note: I first find all the EntityShape instances that are connected with the current ModuleReference and then call the UpdateMainModuleReferencePresentation passing in the new value of the Main property.

2. Register this change rule:

   1: using System;
   2:  
   3: namespace MyModel
   4: {
   5:     /// <summary>
   6:     /// Custom ReportingModel domain model methods.
   7:     /// </summary>
   8:     public partial class ReportingModelDomainModel
   9:     {
  10:         #region Protected Methods
  11:  
  12:         /// <summary>
  13:         /// Returns the non-generated domain model types.
  14:         /// </summary>
  15:         /// <returns>An array of types.</returns>
  16:         protected override Type[] GetCustomDomainModelTypes()
  17:         {
  18:             // Return the rules that should be evaluated for
  19:             // the model
  20:  
  21:             return new System.Type[] 
  22:             {
  23:                 typeof(AssociationConnectorChangeRule), typeof(GenerateDefaultListChangeRule), typeof(ModuleReferenceChangeRule)
  24:             };
  25:         }
  26:  
  27:         #endregion
  28:     }
  29: }

3. Create a new partial class for the EntityShape class and implement the UpdateMainModuleReferencePresentation method:

   1: using System;
   2: using System.Drawing;
   3: using Microsoft.VisualStudio.Modeling.Diagrams;
   4:  
   5: namespace MyModel
   6: {
   7:     /// <summary>
   8:     /// Provides custom code for the EntityShape domain class.
   9:     /// </summary>
  10:     public partial class EntityShape
  11:     {
  12:         #region Internal Methods
  13:  
  14:         /// <summary>
  15:         /// Updates the entity presentation to reflect if the corresponding module reference is the main.
  16:         /// </summary>
  17:         /// <param name="moduleRefereceIsMain">A boolean that indicates if the entity is associated with the main module.</param>
  18:         internal void UpdateMainModuleReferencePresentation(bool moduleRefereceIsMain)
  19:         {
  20:             StyleSet style = this.StyleSet;
  21:             BrushSettings brush = style.GetOverriddenBrushSettings(DiagramBrushes.ShapeBackground);
  22:             if (brush == null)
  23:             {
  24:                 brush = new BrushSettings();
  25:             }
  26:  
  27:             if (moduleRefereceIsMain)
  28:             {
  29:                 brush.Color = Color.FromKnownColor(KnownColor.CadetBlue);
  30:             }
  31:             else
  32:             {
  33:                 brush.Color = Color.FromKnownColor(KnownColor.Lavender);
  34:             }
  35:  
  36:             style.OverrideBrush(DiagramBrushes.ShapeBackground, brush);
  37:         }
  38:  
  39:         #endregion
  40:     }
  41: }

That's it.

Posted 30 October 08 05:43 by hgr | 2 Comments   
Filed under ,
Visual Studio 2008 SP1, TFS, and the Experimental Hive

Every once in a while I enter the nightmare of formatting my development machine. :)

This time my "wonderful nightmare" was getting Visual Studio 2008 SP1 and the Experimental Hive (for my DSL Tools developments) working correctly (with TFS Team Explorer and TFS Source Control working on the Experimental Hive).

In the end, I think that now I would do it correctly. For the sake of documenting the procedure and the potential problems, here it is:

  1. Install Visual Studio 2008 Team System Development Edition
  2. Install Visual Studio 2008 Team Explorer (from the same CD)
  3. Install Visual Studio 2008 SP1
  4. Install Visual Studio 2008 SDK SP1

If I had followed these steps probably I wouldn't have bumped into a set of problems. But, just in case, if you find any of these difficulties after installing all these pieces, here are (possible) solutions...

If you get an WorkItemDeniedOrNotExistsException exception when editing TFS work items:

TFS Team Explorer was probably installed after VS 2008 SP1. Simply reinstall SP1 and it will work.

Reference: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=3380330&SiteID=1

If TFS Team Explorer and Source Control are not available in the Experimental Hive:

There was something wrong with your VS 2008 installation when you installed the SDK. Resetting the Experimental Hive will probably correct the problem. Just point to All Programs \ Microsoft Visual Studio 2008 SDK \ Tools \ Reset the Visual Studio 2008 Experimental Hive.

Reference: http://social.msdn.microsoft.com/Forums/en-US/vsx/thread/b071f63c-e6eb-4ce3-844f-b9345aaf4dec/

If you build a DSL Tools solution and the designer is still not available in the Experimental Hive

Your package was not registered when you built the DSL tools project. Edit the DSLPackage project file and make sure the following piece is set to true:

<RegisterOutputPackage>true</RegisterOutputPackage>

Hope this helps someone out there.

NHibernate and Row Versions (Insert/Update Records)...

I don't like NHibernate! As I use it more and more, I come to terms with this fact and with the fact that I need to find a way to remove it from my projects. :)

My last problem was managing a master/detail/detail relation and making NHibernate insert and/or update the detail records independently from the value of the primary key (which is a Guid).

How? The documentation simply sucks!!! The web site sucks even more!!!

In the end someone had this same problem in the past and found the solution:

When Flushing Goes Bad: Assigned IDs in NHibernate

Note: I preferred a slightly different solution using an Int32 version attribute.

Google is a wonderful thing! :)

Posted 09 October 08 05:10 by hgr | 0 Comments   
Filed under , ,
Dropbox

Some things just work pretty fine!

The first public beta of Dropbox was made available recently.

For me this is particularly useful to share files since I normal don't use more than one computer. Like the images you can see bellow:

 

 

Posted 12 September 08 04:16 by hgr | 0 Comments   
Filed under
Google Chrome

Everybody is talking about this so I had to take a look.

http://www.google.com/chrome

This makes for my 3rd browser. I use Firefox by default, IE8 Beta 2 just because it works better with our intranet, and now Chrome to see if it is that good.

I saw the comics and I see the point of making a multi-process browser. Besides that I don't find any real reason to move away from Firefox, although it is increasingly crashing on my machine (but that's because of some add-ons I use).

Posted 03 September 08 11:35 by hgr | 0 Comments   
Filed under
PicLens for Firefox

As time passes, I find myself doing all the browsing with Firefox.

Today I installed version 3 because a colleague pointed me to the PicLens add-on.

This add-on is simple amazing! Another reason to forget about IE. :)

Click to see full size image

Posted 14 August 08 03:19 by hgr | 0 Comments   
Filed under ,
DSL Tools - Reference of Articles

Since I've been doing a number of posts on the DSL Tools, I thought it would be helpful to have a list with all of them:

Posted 15 July 08 06:43 by hgr | 1 Comments   
Filed under ,
DSL Tools #12 - Cascade Deleting Domain Classes

NOTE: The procedure described here is based on the domain model described in this previous post.

---

Now for something completely different.

As I said before instances of Entity, NormalAttribute, and Calculated attribute are created at runtime when the user "imports" them from another model. When this happens, the entities are also associated with a ModuleReference domain class which is basically a pointer to the external model used to import those classes (see this post for the domain model part that defines that relation).

Basically, this ModuleReference/Entity relation allows the user to know that he's using model X (ModuleReference X) and that Entity Y is defined on that model. Visually this is shown by a simple connector:

Click to see full size image

I allow the user to:

  • Delete module references.
  • Delete entities (meaning that they will not be used in the model).

I don't allow the user to:

  • Remove connections between module references and entities. That connection is important to know from where comes a specific entity.

What happens when the user deletes a module reference? The answer is that I need to delete all entities that came from that model (the entities that are connected with the module reference).

This can be achieved when the user "is deleting" the module reference. For that I just need to override OnDeleting in the GraphHasModuleReferences (the embedding relationship that allows the user to place module references in the model) and do the "cascade delete":

   1: using System;
   2:  
   3: namespace MyDSL
   4: {
   5:     /// <summary>
   6:     /// Provides custom behavior for the GraphHasModuleReferences domain class.
   7:     /// </summary>
   8:     public partial class GraphHasModuleReferences
   9:     {
  10:         #region Public Methods
  11:  
  12:         /// <summary>
  13:         /// Called when an instance of this domain class is being deleted.
  14:         /// </summary>
  15:         protected override void OnDeleting()
  16:         {
  17:             // Default behavior
  18:  
  19:             base.OnDeleting();
  20:  
  21:             // If you delete a module reference then all associated entities need to be removed.
  22:  
  23:             if ((this.Graph != null) && (!this.Graph.IsDeleting))
  24:             {
  25:                 // Ask the user
  26:  
  27:                 string question = string.Format(CommonHelper.Culture, Properties.Resources.RES_ConfirmModuleReferenceDelete, this.ModuleReference.Name);
  28:                 if (CommonHelper.ShowQuestion(question))
  29:                 {
  30:                     // Delete all entities
  31:  
  32:                     for (int i = this.ModuleReference.Entities.Count - 1; i >= 0; i--)
  33:                     {
  34:                         Entity entity = this.ModuleReference.Entities[i];
  35:                         entity.Delete();
  36:                     }
  37:                 }
  38:                 else
  39:                 {
  40:                     throw new InvalidOperationException(Properties.Resources.RES_UserCanceledOperation);
  41:                 }
  42:             }
  43:         }
  44:  
  45:         #endregion
  46:     }
  47: }

Simpler that I thought in the first place.

---

NOTE: The procedure described here is based on the domain model described in this previous post.

Posted 15 July 08 06:36 by hgr | 1 Comments   
Filed under ,
DSL Tools #11 - Preventing the User from Deleting Instances of a Domain Class

NOTE: The procedure described here is based on the domain model described in this previous post.

---

The next thing that I wanted to achieve in my DSL was to prevent the user from deleting manually the instances of certain domain classes.

On my example, since all the attributes are created automatically (by code) when each entity is added (also automatically), I don't want the user to be able to delete these attributes. That would create inconsistencies between this model and the model from where the entities were previously read.

Searching the Web for a solution, I came across 2 different articles about this:

Article #1

Article #2

The two articles define different approaches to the problem. The difference is basically the user experience you'll achieve depending when the solution you select.

Personally, I like to give clear feedback about why he can't do something very obvious. Also I tend to prefer the solution that requires less coding. So I went with the second.

1. Preventing the User from Deleting Embedding Relationships

If you look at the EntityHasNormalAttributes this is an embedding relationship that will be presented to the user has a compartment in the entity shape, containing all its normal attributes.

You prevent the user from deleting normal attributes if you prevent him from deleting this kind of relationship.

For that, you create a partial class for the EntityHasNormalAttributes and override the OnDeleting method. When you want to prevent the delete from happening, you need to throw an exception (InvalidOperationException is recommended). Like this:

   1: using System;
   2: using Microsoft.VisualStudio.Modeling;
   3:  
   4: namespace MyDSL
   5: {
   6:     /// <summary>
   7:     /// Provides custom behavior for the EntityHasNormalAttributes domain class.
   8:     /// </summary>
   9:     public partial class EntityHasNormalAttributes
  10:     {
  11:         #region Public Methods
  12:  
  13:         /// <summary>
  14:         /// Called when an instance of this domain class is being deleted.
  15:         /// </summary>
  16:         protected override void OnDeleting()
  17:         {
  18:             // Default behavior
  19:  
  20:             base.OnDeleting();
  21:  
  22:             // You can't delete normal attributes from entities
  23:  
  24:             if ((this.Entity != null) && (!this.Entity.IsDeleting))
  25:             {
  26:                 throw new InvalidOperationException(Properties.Resources.RES_Error_CannotDeleteAttribute);
  27:             }
  28:         }
  29:  
  30:         #endregion
  31:     }
  32: }

Notice that the exception is only thrown if Entity.IsDeleting is false. This means that you only want to prevent the relationship from being removed if you're not deleting the entity itself.

2. Preventing the User from Deleting Reference Relationships

My domain model also includes a reference relationship that I want to create at runtime (when the user "imports" the model) and prevent from being removed latter by him (manually).

Click to see full size image

In this case I want to prevent the connector from being removed. So the implementation is very similar: override IsDeleting in ModuleReferenceUsesEntities:

   1: using System;
   2:  
   3: namespace MyDSL
   4: {
   5:     /// <summary>
   6:     /// Provides custom behavior for the ModuleReferenceUsesEntities domain class.
   7:     /// </summary>
   8:     public partial class ModuleReferenceUsesEntities
   9:     {
  10:         #region Public Methods
  11:  
  12:         /// <summary>
  13:         /// Called when an instance of this domain class is being deleted.
  14:         /// </summary>
  15:         protected override void OnDeleting()
  16:         {
  17:             // Default behavior
  18:  
  19:             base.OnDeleting();
  20:  
  21:             // You cannot delete connections between module references and entities
  22:             // If we're deleting the module reference, then it's ok
  23:             // If we're deleting the entity, then it's ok
  24:  
  25:             if (this.ModuleReference == null || this.ModuleReference.IsDeleting)
  26:             {
  27:                 return;
  28:             }
  29:             if (this.Entity == null || this.Entity.IsDeleting)
  30:             {
  31:                 return;
  32:             }
  33:  
  34:             throw new InvalidOperationException(Properties.Resources.RES_Error_CannotDeleteModuleReferenceEntityConnection);
  35:         }
  36:  
  37:         #endregion
  38:     }
  39: }

Notice now that the exception is only throw when neither the module reference or the entity is being removed. That is the case when the user tries to delete the connector itself between these two domain classes.

---

NOTE: The procedure described here is based on the domain model described in this previous post.

Posted 15 July 08 06:25 by hgr | 2 Comments   
Filed under ,
DSL Tools #10 - Preventing the User from Adding Instances of a Domain Class

In the next few posts about the DSL tools, I'll be using the following domain model:

Click to see full size image

This model is relatively simple:

  • One model can have multiple entities.
  • One entity can have zero or more normal attributes.
  • One entity can have zero or more calculated attributes.

The interesting thing in this model is that the user is not supposed to create entities or attributes (either normal or calculated).

These domain classes are created at runtime (by code) through a menu operation where the user selects a different model (designed with a different DSL) and the domain classes are read from that model and put in the model the user is working on (so he can change some properties relevant to this model).

Basically this a major requirement of the DSL I've been working on, and that made me find solutions for the problems I'll present in this and in the next few posts.

So, first, if the domain classes are created at runtime, how do I go and prevent the user from adding instances of these classes to the domain model (for example, through the model explorer)?

It's quite simple:

1. Prevent the User from Adding New Entities

Having all the extensibility points in place (see previous posts in this series), all I need to do is to create a partial class for the "container domain class" of Entity. That's ReportingModelRoot (see the GraphHasEntities relation).

On that class, I override the CanMerge method and check if I'm merging with an Entity domain class. If so, the method just has to return false.

   1: using System;
   2: using Microsoft.VisualStudio.Modeling;
   3:  
   4: namespace MyDSL
   5: {
   6:     /// <summary>
   7:     /// Provides custom behavior for the ModuleReferenceUsesEntities domain class.
   8:     /// </summary>
   9:     public partial class ReportingModelRoot
  10:     {
  11:         #region Public Methods
  12:  
  13:         /// <summary>
  14:         /// Returns a value indicating whether the source element represented by the
  15:         /// specified root ProtoElement can be added to this element.
  16:         /// </summary>
  17:         /// <param name="rootElement">The root ProtoElement representing a source element.  This can be null,
  18:         /// in which case the ElementGroupPrototype does not contain an ProtoElements
  19:         /// and the code should inspect the ElementGroupPrototype context information.</param>
  20:         /// <param name="elementGroupPrototype">The ElementGroupPrototype that contains the root ProtoElement.</param>
  21:         /// <returns>
  22:         /// true if the source element represented by the ProtoElement can be added to this target element.
  23:         /// </returns>
  24:         protected override bool CanMerge(ProtoElementBase rootElement, ElementGroupPrototype elementGroupPrototype)
  25:         {
  26:             // The user cannot add entities (they're added by code)
  27:  
  28:             if (rootElement.DomainClassId.Equals(Entity.DomainClassId))
  29:             {
  30:                 return false;
  31:             }
  32:  
  33:             // Default behavior
  34:  
  35:             return base.CanMerge(rootElement, elementGroupPrototype);
  36:         }
  37:  
  38:         #endregion
  39:     }
  40: }

2. Prevent the User from Adding Attributes to an Entity

This is basically the same as in the previous case. The difference is just that the "container class" is now Entity:

   1: using System;
   2: using Microsoft.VisualStudio.Modeling;
   3:  
   4: namespace MyDSL
   5: {
   6:     /// <summary>
   7:     /// Provides custom behavior for the Entity domain class.
   8:     /// </summary>
   9:     public partial class Entity
  10:     {
  11:         #region Public Methods
  12:  
  13:         /// <summary>
  14:         /// Determines whether this instance can merge the specified root element.
  15:         /// </summary>
  16:         /// <param name="rootElement">The root element.</param>
  17:         /// <param name="elementGroupPrototype">The element group prototype.</param>
  18:         /// <returns>
  19:         ///     <c>True</c> if this instance can merge the specified root element; otherwise, <c>false</c>.
  20:         /// </returns>
  21:         protected override bool CanMerge(ProtoElementBase rootElement, ElementGroupPrototype elementGroupPrototype)
  22:         {
  23:             // The user cannot add normal attributes (they're added by code)
  24:             // The user cannot add calculated attributes (they're added by code)
  25:  
  26:             if (rootElement.DomainClassId.Equals(NormalAttribute.DomainClassId) || rootElement.DomainClassId.Equals(CalculatedAttribute.DomainClassId))
  27:             {
  28:                 return false;
  29:             }
  30:  
  31:             // Default behavior
  32:  
  33:             return base.CanMerge(rootElement, elementGroupPrototype);
  34:         }
  35:  
  36:         #endregion
  37:     }
  38: }

One important thing to notice here is that this behavior is just UI related. It disables are commands in the designer (and the model explorer) that would allow the user to add instances of these domain classes. It doesn't prevent code from adding instances of these same classes. But that was just what I needed (since I have a procedure that does exactly that).

Posted 15 July 08 06:08 by hgr | 3 Comments   
Filed under ,
DSL Tools #9 - Adding Context Menus to a Designer

Again working on a new DSL, I was looking for a simple way of adding menus to the my DSL's main designer context menu.

In the end I found this great article by Sebastian Talamoni that explains it very well.

Here it is:

Adding a Menu to a DSL

Posted 14 July 08 03:57 by hgr | 2 Comments   
Filed under ,
DSL Tools #8 - Multiple Outputs from Single Text Template

One of my colleagues pointed me this article by Oleg Sych:

How to generate multiple outputs from single T4 template

The technique is complex but it does solve one of the worst limitations of the DSL Tools. Nice work.

Posted 20 June 08 05:20 by hgr | 1 Comments   
Filed under ,
More Posts Next page »