The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.
RemoteTextMixinBase
).DataCache
.QueryAspect
to show read-only views of many-to-many relationsIMetaReadable.GetValue()
behavior is inconsistent with relations and methods. The behavior or reading a related object or list using GetValue() has changed. Previously, the value returned non-null only if it had been caused to load by using GetObject()
or GetList()
) or if it had a ValueGenerator
. If it was generated with a ValueGenerator
, it was always regenerated, ignoring the value of ValueGenerationFrequency
.Equals()
method, which resulted in unpredictable behavior with hash tables, sets or dictionaries. This has been fixed, but applications that relied on the formerly unpredictable behavior will now function differently.DatabaseState
no longer exists. It has been replaced with ExternalResourceState
.IAuthenticator<TApplication>
, IAuthorizer<TApplication>
and ILoginValidator<TApplication>
interfaces have all acquired a method with interface void LoadSettings(TConfiguration configuration, IMessageRecorder recorder)
.Encodo.Data.SessionFlags
have been moved to Encodo.Quino.Data.Persistence.SessionFlags
ICoreConfiguration.GetConnectionDetails()
no longer returns a simple string, but a sequence of strings. To restore , use string.Join("; "_configuration.GetConnectionDetails())
EnryptionType
was moved from Encodo.Security
to Encodo.Encryption
DirectoryServiceSettings.ServerUri
is deprecated; use HostName
instead.MetaBuilderModuleTools
has been renamed to MetaBuilderTools
.MetaBuilderBasedMetadataGeneratorBase.ConfigureBuilder() no longer exists; instead, override MetaBuilderBasedMetadataGeneratorBase.RegisterDependencies()
.MetaBuilder.AddMultiLanguageProperty()
now requires a base Guid
parameter.MetaBuilder.AddWrapperClass()
no longer requires a Guid
parameterThe summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.
ShortCircuit
event type (replaced with primary data handler)No known breaking changes
The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.
No known breaking changes
For a long time, we maintained our release notes in a wiki that's only accessible for Encodo employees. For the last several versions -- since v1.7.6 -- we've published them on the web site, available for all to see.1 Because they reveal the progress and history of Quino quite nicely, we've made all archival release notes available on the web site as well.
Browse to the Quino folder in the blogs to see them all. Some highlights are listed below:
Please note that the detailed release notes include links to our issue-tracking system, for which a login is currently required.↩
The initial version of Quino was developed while working on a suite of applications for running a medium-sized school. The software includes teacher/student administration, curriculum management, scheduling and planning as well as integration with mail/calendar and schedule-display systems. By the time we finished, it also included a web interface for various reporting services and an absence-tracking and management system is going to be released soon.↩
The release notes were not available at publication time.↩
Visual Studio bietet die Möglichkeit, zusätzlich zu den von Haus aus mitgelieferten Projekt-Templates eigene Templates zu erstellen und diese dann zu verwenden. Dies ist von Vorteil, wenn häufig ähnliche Projekte erstellt werden und das Projektsetup verhältnismässig aufwändig ist. Diese Voraussetzugen treffen auf unser hauseigenenes Framework Quino bestens zu: Für jede neue Quino Applikation muss ein Model erstellt werden, was jeweils einige Code- und Konfigurationsdateien erfordert. Dies nimmt, insbesondere wenn man es zum ersten Mal macht, schnell einige Stunden Zeit in Anspruch bis alles wie gewünscht läuft.
Mit einem Projekt-Template ist dies deutlich einfacher: Neues Projekt erstellen, die gewünschten Module wählen und schon wird eine lauffähige Quino Applikation mit einem einfachen Model erstellt. Darauf aufbauend kann man dann die eingene Applikation implementieren.
Die Erstellung eines eigenen Projekt-Templates ist aber, vor Allem wenn es etwas umfangreicher ist und zusätzlich einen eigenen Wizard haben soll, mit einigen Fallstricken versehen. Auch sind die Informationen dazu auf MSDN und generell im Internet eher spärlich und teilweise verwirrend. Um das gewonnene Know-How mit anderen Entwicklern zu teilen haben wir eine kleine Anleitung verfasst. Diese ist zwar auf Quino zugeschnitten, kann aber natürlich auch auf eigene Bedürfnisse angepasst werden.
Es soll ein Projekt-Template erstellt werden, das eine lauffähige Quino Applikation generiert. Dabei sollen die Dateinamen sowie die verwendeten Namespaces dem Namen des Projektes angepasst werden. Ausserdem soll das Projekt-Template beim Generieren des Projekts einen Wizard anzeigen, in dem verschiedenen Quino Module an- oder abgewählt werden können. Momentan sind dies Core für das Model und Winforms für eine Winforms Oberfläche. Für jedes gewählte Modul wird ein Projekt in der Solution erstellt und jeweils alle benötigten Referenzen richtig gesetzt.
Diese Anleitung geht davon aus, dass Visual Studio 2012 mit allen aktuellen Updates installiert ist. Zusätzlich wird das Microsoft Visual Studio 2012 SDK benötigt. Dies stellt den Projekttyp Project Template zur Verfügung. Ausserdem ist es ratsam, ein Projekt zu erstellen das genau dem Projekt entspricht, das nachher generiert werden soll.
Ein Projekt-Template ist im Grunde nichts anderes als eine Zip Datei, in welche alle benötigten Dateien gepackt sind. Von zentraler Bedeutung sind hier die .vstemplate-Dateien, von welchen jedes Projekt-Template mindestens eine beinhalten muss. Innerhalb einer .vstemplate-Datei kann dann wiederum auf andere .vstemplate-Dateien verlinkt werden um so Subprojekte zu generieren. Ausserdem sind in den .vstemplate-Dateien die Metadaten der einzelnen Projekte hinterlegt. Dies ist für die Haupt-.vstemplate-Datei besonders wichtig, da diese Daten im New 'Project Dialog' von Visual Studio angezeigt werden. Dazu gehören der Name des Projekts, eine kurze Beschreibung, ein Icon sowie eine grössere Grafik welche beispielsweise einen Screenshot oder ein Logo enthalten kann. Ausserdem sind alle Dateien enthalten, welche später in das neue Projekt eingefügt werden. Alle Dateien können mit Platzhalter versehen werden, welche dann bei der Generierung des Projekts durch die entsprechenden Werte ersetzt werden.
Dateisystem (nicht vollständig):
QuinoTemplate.vstemplate
__PreviewImage.png
__TemplateIcon.png
Core/Core.vstemplate
Core/Quino.Core.vstemplate
Core/App/QuinoConfiguration.cs
Core/Models/QuinoModelClasses.cs
...
Core/Models/Generators/QuinoCoreGenerator.cs
...
Winform/Winform.vstemplate
Winform/Quino.Winform.App.csproj
Winform/Program.cs
Winform/data-configuration.xml
...
Winform/Forms/MainForm.cs
...
QuinoTemplate.vstemplate:
<VSTemplate Version="3.0.0" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005" Type="ProjectGroup">
<TemplateData>
<Name>Quino Application</Name>
<Description>A quino application with different modules.</Description>
<ProjectType>CSharp</ProjectType>
<ProjectSubType>
</ProjectSubType>
<SortOrder>1000</SortOrder>
<CreateNewFolder>true</CreateNewFolder>
<DefaultName>MyQuinoApplication</DefaultName>
<ProvideDefaultName>true</ProvideDefaultName>
<LocationField>Enabled</LocationField>
<EnableLocationBrowseButton>true</EnableLocationBrowseButton>
<Icon>__TemplateIcon.png</Icon>
<PreviewImage>__PreviewImage.png</PreviewImage>
</TemplateData>
<TemplateContent>
<ProjectCollection>
<ProjectTemplateLink ProjectName="$quinoapplicationname$.Core">
Core\Core.vstemplate
</ProjectTemplateLink>
<ProjectTemplateLink ProjectName="$quinoapplicationname$.Winform">
Winform\Winform.vstemplate
</ProjectTemplateLink>
</ProjectCollection>
</TemplateContent>
</VSTemplate>
Die Namen der Projekte können durch Platzhalter beeinflusst werden.
Core.vstemplate (stellvertretend für die Subprojekte):
<VSTemplate Version="3.0.0" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005" Type="Project">
<TemplateData>
<Name>Quino Core</Name>
<Description>Provides a quino core project containing the model generation</Description>
<ProjectType>CSharp</ProjectType>
<ProjectSubType>
</ProjectSubType>
<SortOrder>1000</SortOrder>
<CreateNewFolder>true</CreateNewFolder>
<DefaultName>MyQuinoApplication.Core</DefaultName>
<ProvideDefaultName>true</ProvideDefaultName>
<LocationField>Enabled</LocationField>
<EnableLocationBrowseButton>true</EnableLocationBrowseButton>
</TemplateData>
<TemplateContent>
<Project
TargetFileName="$quinoapplicationname$.Core.csproj"
ProjectName="$quinoapplicationname$.Core"
File="Quino.Core.csproj"
ReplaceParameters="true">
<Folder Name="App" TargetFolderName="App">
<ProjectItem ReplaceParameters="true" TargetFileName="$quinoapplicationname$Configuration.cs">
QuinoConfiguration.cs
</ProjectItem>
</Folder>
<Folder Name="Models" TargetFolderName="Models">
<Folder Name="Generators" TargetFolderName="Generators">
<ProjectItem ReplaceParameters="true" TargetFileName="$quinoapplicationname$CoreGenerator.cs">
QuinoCoreGenerator.cs
</ProjectItem>
...
</Folder>
<ProjectItem ReplaceParameters="true" TargetFileName="$quinoapplicationname$ModelClasses.cs">
QuinoModelClasses.cs
</ProjectItem>
...
</Folder>
</Project>
</TemplateContent>
</VSTemplate>
Die Namen der generierten Dateinen können ebenfalls durch Platzhalter beeinflusst werden.
Auch innerhalb der einzelnen Codedateien können Platzhalter verwendet werden die dann beim Generieren des Projekts durch die entsprechenden Variablen erstetzt werden:
using Encodo.Quino.Meta;
namespace $quinoapplicationname$.Models
{
public class $quinoapplicationname$ModelClasses
{
public IMetaClass Company { get; set; }
public IMetaClass Person { get; set; }
}
}
Damit die einzelnen Quino Module an- oder abgewählt werden können sowie um dem Anwender andere Konfigurationsmöglichkeiten - wie etwa das Auswählen des Namespaces - zu geben kann man das Projekt-Template mit einem eigenen Wizard versehen. Dieser wird jedes Mal angezeigt, wenn aus dem Projekt-Template ein neues Projekt erzeugt wird. Der Wizard für das Projekt-Template ist grundsätzlich eine ganz normale .Net Anwendung welche in eine DLL kompiliert und dann im Template registriert wird.
Ein eigener Wizard für das Projekt-Template kann implementiert werden indem man vom Interface Microsoft.VisualStudio.TemplateWizard.IWizard
ableitet. Da das Quino Projekt-Template momentan zwei Module (Core und Winform) generieren kann werden insgesamt drei Wizards gebraucht:
Die beiden Sub-Wizards sind nötig, weil der Hauptwizard keinen Zugriff auf die Replace-Parameter der SubProjekt-Templates hat. Damit der Hauptwizard mit den Subwizards kommunizieren kann ist ein kleiner Trick nötig: Die im Hauptwizard getätigten Einstellungen werden in public static
Parametern abgelegt auf diese wiederum die beiden Subwizards zugreifen können. Dies funktioniert, weil alle drei Wizards in der gleichen Runtime Umgebung laufen.
QuinoWizard.cs:
public class QuinoWizard : IWizard
{
#region Implementation of IWizard
public void RunStarted(
object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind,
object[] customParams)
{
try
{
using (var inputForm = new UserInputForm())
{
// Der Winforms Dialog wird angezeigt und die Einstellungen des
// Benutzers werden in public static Parametern abgelegt.
inputForm.ShowDialog();
GenerateCore = inputForm.GenerateCore;
GenerateWinform = inputForm.GenerateWinform;
QuinoApplicationName = inputForm.DefaultNamespace;
EncodoSourceRoot = inputForm.EncodoSourceRoot;
// Die Parameter werden in das replacementsDictionary übernommen.
Tools.SetReplacementParameters(replacementsDictionary);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
}
public bool ShouldAddProjectItem(string filePath)
{
return true;
}
// Alle anderen implementierten Methoden haben einen leeren Methodenrumpf.
#endregion
public static string QuinoApplicationName { get; private set; }
public static bool GenerateCore { get; private set; }
public static bool GenerateWinform { get; private set; }
public static string EncodoSourceRoot { get; private set; }
}
CoreWizard.cs (stellvertretend für die beiden Subwizards):
{
#region Implementation of IWizard
public void RunStarted(
object automationObject,
Dictionary<string, string> replacementsDictionary,
WizardRunKind runKind,
object[] customParams)
{
if (!QuinoWizard.GenerateCore)
{
// So wird die Generierung des Subprojekts gegebenenfalls verhindert.
throw new WizardCancelledException();
}
// Die Parameter werden in das replacementsDictionary übernommen.
Tools.SetReplacementParameters(replacementsDictionary);
}
public bool ShouldAddProjectItem(string filePath)
{
return true;
}
// Alle anderen implementierten Methoden haben einen leeren Methodenrumpf.
#endregion
}
Der Vollständigkeit halber die Methode Tools.SetReplacementParameters:
public static void SetReplacementParameters(Dictionary<string, string> replacementsDictionary)
{
replacementsDictionary.Add("$quinoapplicationname$", QuinoWizard.QuinoApplicationName);
replacementsDictionary.Add("$encodosourceroot$", QuinoWizard.EncodoSourceRoot);
}
Damit der Wizard vom Projekt-Template aufgerufen werden kann muss er im Global Assembly Cache (GAC) registriert werden. Dazu das Wizward Projekt kompilieren, den Visual Studio Command Prompt im Administratormodus öffnen und das Assembly registrieren:
gacutil -i Wizard.dll
In den vorherigen beiden Schritten wurde für jedes Subprojekt sowie für das Hauptprojekt jeweils ein Wizard erstellt. Diese Wizwards müssen jetzt in den einzelnen .vstemplate-Dateien verlinkt werden. Dazu jeweils nach dem Knoten
QuinoTemplate.vstemplate:
<WizardExtension>
<Assembly>Wizard, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=[PublicKey]</Assembly>
<FullClassName>Wizard.QuinoWizard</FullClassName>
</WizardExtension>
Core.vstemplate:
<WizardExtension>
<Assembly>Wizard, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=[PublicKey]</Assembly>
<FullClassName>Wizard.CoreWizard</FullClassName>
</WizardExtension>
Winform.vstemplate:
<WizardExtension>
<Assembly>Wizard, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=[PublicKey]</Assembly>
<FullClassName>Wizard.WinformWizard</FullClassName>
</WizardExtension>
Der Public Key ist in allen drei Fällen gleich und kann beispielsweise mit Tools wie dotPeek von JetBrains ermittelt werden. Der FullClassName hingegen muss auf den jeweiligen Wizard des Templates verweisen.
Um das fertige Template zu testen bietet Visual Studio einige praktische Hilfsmittel. So kann kann man einfach F5 drücken und eine neue Instanz von Visual Studio wird hochgezogen in der das neue Template bereits registriert ist. Dies bietet den Vorteil, dass man die Generierung der Projekte debuggen kann und so etwaige Fehler einfach findet.
Wenn das Template fertig ist und funktioniert kann man das Template im Release-Modus builden woraufhin ein Zip-File mit dem Projekt-Template generiert wird. Dieses kann man dann entweder in den Ordner %UserProfile%\Documents\Visual Studio 2012\Templates\ProjectTemplates\Visual C#
kopieren wo das Projekt für den aktuellen Benutzer zur Verfügung steht oder in den Ordner %ProgramFiles%\Microsoft Visual Studio 11.0\Common7\IDE\ProjectTemplates\CSharp
woraufhin das neue Projekt-Template dann bei allen Benutzern im New Project Dialog erscheint.
Das Erstellen eines eigenen Projekt-Templates ist insbesondere für Frameworkentwickler eine gute Möglichkeit, den Umgang mit dem Framework zu erleichtern. Der Anwender kann so mit wenigen Mausklicks eine funktionsfähige Applikation erstellen und sieht auch gleich die grundlegenden Designpatterns. Dies ermöglicht es ihm dann, die Anwendung nach seinen eigenen Bedürfnissen zu erweitern.
Natürlich bedeutet das Erstellen und Unterhalten eines eigenen Projekt-Templates auch einigen Aufwand, da das Template für jede neue Quino Version wieder geprüft und gegebenenfalls angepasst werden muss. Hier bietet es sich an, diesen Prozess zu automatisieren. Das Projekt-Template kann auf einem Buildserver vollautomatisch gebuildet und daraus dann ein Projekt generiert werden. Dieses kann der Buildserver dann wiederum builden und so prüfen, ob das Projekt-Template noch funktionsfähig ist.
Um die Installation des Templates auf verschiedenen Rechnern zu vereinfachen haben wir zusätzlich ein Installationsprogramm erstellt der den Wizard im GAC registriert und das Projekt-Template ins richtige Verzeichnis kopiert.
Insgesamt war das Erstellen des Projekt-Templates für Quino zwar aufwändig, ich würde aber auf jeden Fall sagen, dass der dadurch gewonnene Nutzen den Aufwand mehr als wett macht.
The instructions below explain how to set up a Quino application to use a local database driver in Quino 1.8.5 and higher.
The implementation in this version uses a Mongo database as a backing store, so there a few limitations of which you should be aware:
The Mongo driver sounds quite limited compared to a full-fledged driver like that for PostgreSql or SQL Server. So why would you want to use it? There are situations where non-ACID, schema-less persistence is acceptable. In these cases, the lack of support for the features listed above is not a deal-breaker.1
The following situations lend themselves to using a local database:
If you're convinced, you can try it out in your own application by following the instructions below.
configuration.IntegrateLocalDatabase()
(an extension method defined in Encodo.Quino.App.MetaConfigurationTools
). This simply includes code in the application startup that will start and run a local Mongo database if it detects that the configuration requires it. That's all the .NET code you have to write; the rest is configuration.<Mongo>
<Title>Mongo</Title>
<typename>Encodo.Quino.Data.Mongo.MongoMetaDatabase, Quino.Data.Mongo</typename>
<Resource>MyAppDatabaseName</Resource>
</Mongo>
You can change the resource name to something that makes sense for your application. It doesn't really matter because, by default, the database is stored in the user's AppData/Local/...
folder and they never see the name anyway4.Again in the configuration file, set the default connection settings to "Mongo":
<?xml version="1.0" encoding="utf-8" ?>
<config>
<servers>
<default>Mongo</default>
<!-- other connection settings -->
<Mongo>
<Title>Mongo</Title>
<typename>Encodo.Quino.Data.Mongo.MongoMetaDatabase, Quino.Data.Mongo</typename>
<Resource>MyAppDatabaseName</Resource>
</Mongo>
</servers>
</config>
Let your application know where to find the Mongo executable. You can either copy the Mongo database daemon to Mongo\mongod.exe
next to your application executable or you can specify a location from the configuration file, as shown below.
<mongo>
<executable>C:\Tools\MongoDB\bin\mongod.exe</executable>
</mongo>
Optional: choose a location to store the local database. By default, the database is stored in a "Data" subfolder of the user's local data for the application. You can specify an alternate location from the configuration file5, as shown below.
<mongo>
<executable>C:\Tools\MongoDB\bin\mongod.exe</executable>
<databasepath>U:\Bob\Prototype23\Data</databasepath>
</mongo>
Now you can start your application and store data locally using Mongo.
Happy modeling!
Mongo is also quite fast -- partly due to the fact that it doesn't support transactions and doesn't need to check foreign keys. Just to be clear, we understand that, for many large-data situations, a fast, non-ACID driver is exactly what you want. That's kind of why Quino supports Mongo out-of-the-box.↩
Since Quino also supports remoting out of the box, you could also run a server either on your own infrastructure or in the cloud (Azure) but that involves a lot more work. It's also not guaranteed because the customer may not have access to the server from their internal network.↩
This is becoming more and more common as some developers use super-lightweight netbooks without a lot of memory. Naturally, we recommend that developers work with the primary target database as much as possible.↩
Unless it's shown in the title bar of the main window in debug mode or in the about window↩
Be aware that, since each running instance of the application has its own Mongo database daemon, it is not possible for multiple users to access data in the same directory. You still need a server for that.↩
The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.
IMetaModule.ClassOptions.OneClassPerFile = true
for those modules where it is desired).IConstantExpression
no longer exists. Please update method signatures with IExpression
instead.Encodo.Quino.Meta.IMetaModuleGenerator
has been moved to Encodo.Quino.Tools.ModelGenerators.IMetaModuleGenerator
. Fix compile errors by including the new namespace.MetaBuilderBase
no longer returns MetaGroupContainer
; instead it returns IMetaGroupContainer
. Code that expects the class rather than the interface will have to be updated to use the interface instead.MetaBuilder.CreateWrapperClass()
no longer adds the created class to the model. If this is the desired behavior, use MetaBuilder.AddWrapperClass()
instead. In most cases, the extra class in the model was neither necessary nor desired, but we strongly recommend that you verify your calls to CreateWrapperClass()
to make sure that it still does what you expect.IMetaMethodImplementationContainer
interface. There is only one method to implement -- SetSession()
-- and you can use the MetaMethodImplementationContainer
base class if you don't have another base class that you want to use. Regenerate code to update the generated class for remotable methods to a version that implements the interface.The generated remote methods can no longer be called without specifying the session to use for the remote-method call. For a given method interface -- IBusinessLogic
say -- instead of calling ServiceLocator.GetInstance<IBusinessLogic>()
to get the instance, use the helper method MetaMethodTools.GetInstance<TIBusinessLogic>(session)
instead.For example, in the Quino demo, the call to DeletePerson
used to be:
ServiceLocator.Current.GetInstance<IBusinessLogicMethods>().DeletePerson(person);
Instead, you should call:
MetaMethodTools.GetInstance<IBusinessLogicMethods>(person.Session).DeletePerson(person);
```**XML documentation** files are **no longer included** with the **source-only** release. Instead, you should generate the documentation files locally by calling the `deploydoc` target in the `Quino.build` NAnt file. This target can be called at any time to synchronize the XML documentation files with the current sources.
For example, when you download the Quino sources (and have NAnt in the system `PATH`), you can execute the following to build & deploy Quino as well as source-code documentation.
nant deploydoc deploy
* As a result of updates made for the model-generation pattern, you will have to re-generate code for your model(s) in order to remove compiler warnings in that code.
------------------------------------------------------------------------
[^1]: Naturally, the Mongo driver can also be used in a distributed solution. It is, as much as possible, a first-class driver for Quino with the following caveats: it supports neither foreign-key constraints nor transactions.
In the latest version of Quino -- version 1.8.5 -- we took a long, hard look at the patterns we were using to create metadata. The metadata for an application includes all of the usual Quino stuff: classes, properties, paths, relations. With each version, though we're able to use the metadata in more places. That means that the metadata definition code grows and grows. We needed some way to keep a decent overview of that metadata without causing too much pain when defining it.
In order to provide some background, the following are the high-level requirements that we kept in mind while designing the new pattern and supporting framework.
Manage complexity
A simple model should be easy and straightforward to build, with no cumbersome boilerplate; complex models should support multiple layers and provide an overview
Leverage existing knowhow
Our users don't want to learn a new language/IDE in order to create metadata; neither do we want to provide support for our own metadata-definition language
Support modularization
Modules can be used to hide complexity but are also sometimes necessary to define hard boundaries in the application metadata
Support extensibility
Interdependent modules and overlays will need to refer to elements in other modules; there needs to be a standard mechanism for defining and accessing metadata elements that doesn't rely on string constants1
Support refactoring
Rely on convention and name-matching as little as possible to avoid subtle errors
Support introspection
Developers that stick to the pattern should be able to maximize efficiency using common navigation and introspection2 tools like Visual Studio or ReSharper.
Quino metadata has always been defined using a .NET language -- in our case, we always use C# to define the metadata, using the MetaBuilder
or InMemoryMetaBuilder
to compose the application model. This approach satisfies the need to leverage existing tools, refactoring and introspection.
Since Quino metadata is an in-memory construct, there will always be a .NET API for creating metadata. This is not to say that there will never be a DSL to define Quino metadata but that such an approach is not the subject of this post.
Quino applications have always been able to define and integrate metadata modules (e.g. reporting or security) using an IMetaModuleBuilder
. Modules solved interdependency issues by splitting the metadata-generation into several phases:
In this way, when a module needed to add a path between a class that it had defined and a class defined in another module, it could be guaranteed that classes and foreign keys for all modules had been defined before any paths were created. Likewise for classes that wanted to define relations based on paths defined in other modules.
The limitation of the previous implementation was that a module generator always created its own module and builder and could not simply re-use those created by another generator. Basically, there was no "lightweight" way of splitting metadata-generation into separate files for purely organizational purposes.
There were also a few issues with the implementation of the main model-generation code as well. The previous pattern depended heavily on local variables, all defined within one mammoth function. Separating code into individual method calls was ad-hoc -- each project did it a little differently -- and involved a lot of migration of local variables to instance variables. With all code in a single method, file-structure navigation tools couldn't help at all. The previous pattern prescribed using file comments or regions that could be located using "find in file". This was clearly sub-optimal.
The new pattern that can be applied for all models, bit or small includes the following parts:
Model generator
As before, there is a class that implements the IMetaModelGenerator
interface. This class is used by the application configuration and various tools (e.g. the code generator or UML generator) to create the model.
Model elements
Metadata that is referenced from multiple steps in the metadata-generation process is stored in a separate object (or objects) called the model elements. (E.g. classes are created in the AddClasses()
step and referenced in the AddPaths
, AddProperties
and AddLayouts
steps.) The model elements typically has two properties called Classes
and Paths
.
Metadata generators
Module generators still exist, but there are now also metadata generators that are lightweight, using a metadata builder and elements defined by another generator (typically a module generator or the model generator itself).
This may sound like a lot of overhead for a simple application, but it's really not that much extra code. The benefits are:
But enough chatter; let's take a look at the absolute minimum boilerplate for an empty model.
public class DemoModelElements
{
public DemoModelElements()
{
Classes = new DemoModelClasses();
Paths = new DemoModelPaths();
}
public DemoModelClasses Classes { get; private set; }
public DemoModelPaths Paths { get; private set; }
}
public class DemoModelPaths
{
}
public class DemoModelClasses
{
}
public class DemoCoreGenerator : DependentMetadataGeneratorBase<DemoModelGenerator, DemoModelElements, MetaBuilder>
{
}
public class DemoModelGenerator : MetaBuilderBasedModelGeneratorBase<DemoModelElements>
{
protected override void AddMetadata()
{
Builder.Include<DemoCoreGenerator>();
}
}
The code above is functional but doesn't actually create any metadata. So what does it do?
MetaBuilderBasedModelGeneratorBase
to indicate the type of Elements that will be exposed by this model generator. The elements class is created automatically and is available as the property Elements
(as we'll see in the examples below). Additionally, we're using a ModelGeneratorBase
that is based on a MetaBuilder
which means that the property Builder
is also available and is of type MetaBuilder
.DemoCoreGenerator
which is a dependent generator -- it's lightweight and uses the elements and builder from its owner. The exact types are shown in the class declaration; it can be read as: get elements of type DemoModelElements
and a builder of type MetaBuilder
from the generator with type DemoModelGenerator
. The initial generic argument can be any other metadata generator that implements the IElementsProvider<TElements, TBuilder>
interface.AddMetadata
to include the metadata created by DemoCoreGenerator
in the model.Even though it's not very much code, you can create a snippet or a file template with Visual Studio or a Live Template or file template with ReSharper to quickly create a new model.
Now, let's fill the empty model with some metadata. The first step is to define the model that we're going to build. That part goes in the AddMetadata()
method.3
public class DemoModelGenerator : MetaBuilderBasedModelGeneratorBase<DemoModelElements>
{
protected override void AddMetadata()
{
Builder.CreateModel<DemoModel>("Demo", /*Guid*/);
Builder.CreateMainModule("Encodo.Quino");
Builder.Include<DemoCoreGenerator>();
}
}
A typical next step is to define a class. Let's do that.
public class DemoModelClasses
{
public IMetaClass Company { get; set; }
}
public class DemoCoreGenerator : DependentMetadataGeneratorBase<DemoModelGenerator, DemoModelElements, MetaBuilder>
{
protected override void AddClasses()
{
Elements.Classes.Company = Builder.AddClassWithDefaultPrimaryKey("Company", /*Guid*/, /*Guid*/);
}
}
As you can see, we added a new class to the elements and created and assigned it in the AddClasses()
phase of metadata-generation.
An obvious next step is to create another class and define a path between them.
public class DemoModelClasses
{
public IMetaClass Company { get; set; }
public IMetaClass Person { get; set; }
}
public class DemoCoreGenerator : DependentMetadataGeneratorBase<DemoModelGenerator, DemoModelElements, MetaBuilder>
{
protected override void AddClasses()
{
Elements.Classes.Company = Builder.AddClassWithDefaultPrimaryKey("Company", /*Guid*/, /*Guid*/);
Elements.Classes.Person = Builder.AddClassWithDefaultPrimaryKey("Person", /*Guid*/, /*Guid*/);
Builder.AddInvisibleProperty(Elements.Classes.Person, "CompanyId", MetaType.Key, true, /*Guid*/);
}
protected override void AddPaths()
{
Elements.Paths.CompanyPersonPath = Builder.AddOneToManyPath(
Elements.Classes.Company, "Id",
Elements.Classes.Person, "CompanyId",
/*Guid*/, /*Guid*/
);
}
}
Having a path is not enough, though. We can also define how the relations on that path are exposed in the classes.
public class DemoCoreGenerator : DependentMetadataGeneratorBase<DemoModelGenerator, DemoModelElements, MetaBuilder>
{
protected override void AddClasses()
{
Elements.Classes.Company = Builder.AddClassWithDefaultPrimaryKey("Company", /*Guid*/, /*Guid*/);
Elements.Classes.Person = Builder.AddClassWithDefaultPrimaryKey("Person", /*Guid*/, /*Guid*/);
Builder.AddInvisibleProperty(Elements.Classes.Person, "CompanyId", MetaType.Key, true, /*Guid*/);
}
protected override void AddPaths()
{
Elements.Paths.CompanyPersonPath = Builder.AddOneToManyPath(
Elements.Classes.Company, "Id",
Elements.Classes.Person, "CompanyId",
/*Guid*/, /*Guid*/
);
}
protected override void AddProperties()
{
Builder.AddRelation(Elements.Classes.Company, "People", "", Elements.Paths.CompanyPersonPath);
Builder.AddRelation(Elements.Classes.Person, "Company", "", Elements.Paths.CompanyPersonPath);
}
}
OK, now we have a model with two entities -- companies and people -- that are related to each other so that a company has a list of people and each person belongs to a company.
Now we'd like to make the metadata support German as well as English. Quino naturally supports more generalized ways of doing this (e.g. importing from files), but let's just add the metadata manually to see what that would look like (unaffected methods are left off for brevity).
public class DemoModelElements
{
public DemoModelElements()
{
Classes = new DemoModelClasses();
Paths = new DemoModelPaths();
}
public ILanguage English { get; set; }
public ILanguage German { get; set; }
public DemoModelClasses Classes { get; private set; }
public DemoModelPaths Paths { get; private set; }
}
public class DemoCoreGenerator : DependentMetadataGeneratorBase<DemoModelGenerator, DemoModelElements, MetaBuilder>
{
protected override void AddCoreElements()
{
Elements.English = Builder.AddDisplayLanguage("en-US", "English");
Elements.German = Builder.AddDisplayLanguage("de-CH", "Deutsch");
}
protected override void AddClasses()
{
var company = Elements.Classes.Company = Builder.AddClassWithDefaultPrimaryKey("Company", /*Guid*/, /*Guid*/);
company.Caption.SetValue(Elements.English, "Company");
company.Caption.SetValue(Elements.German, "Firma");
company.PluralCaption.SetValue(Elements.English, "Companies");
company.PluralCaption.SetValue(Elements.German, "Firmen");
var person = Elements.Classes.Person = Builder.AddClassWithDefaultPrimaryKey("Person", /*Guid*/, /*Guid*/);
Builder.AddInvisibleProperty(person, "CompanyId", MetaType.Key, true, /*Guid*/);
person.Caption.SetValue(Elements.English, "Person");
person.Caption.SetValue(Elements.German, "Person");
person.PluralCaption.SetValue(Elements.English, "People");
person.PluralCaption.SetValue(Elements.German, "Personen");
}
}
Note that I created a local variable for both company and person. I did this for two reasons:
Elements.Classes.Person
and Elements.Classes.Company
properties. It's useful to keep the number of references to a minimum in order to make searching for usages with a tool like ReSharper of maximum benefit. Otherwise, there's a lot of noise to signal and you'll get hundreds of references when there are only actually a few dozen "real" references.You can see that the metadata-generation code is still manageable, but it's growing. Once we've filled out all of the properties, relations, translations, layouts and view aspects for the person and company classes, we'll have a file that's several hundred lines long. A file of that size is still manageable and, since we have methods, it's eminently navigable with a file-structure browser.
If we don't mind keeping -- or we'd rather keep -- everything in one file, we can see more structure by splitting the code into more methods. This is really easy to do because we're using the elements to reference other parts of metadata instead of local variables. For example, let's move the class initialization code for the person and company entities to separate methods (unaffected methods are left off for brevity).
public class DemoCoreGenerator : DependentMetadataGeneratorBase<DemoModelGenerator, DemoModelElements, MetaBuilder>
{
protected override void AddClasses()
{
AddCompany();
AddPerson();
}
private void AddCompany()
{
var company = Elements.Classes.Company = Builder.AddClassWithDefaultPrimaryKey("Company", /*Guid*/, /*Guid*/);
company.Caption.SetValue(Elements.English, "Company");
company.Caption.SetValue(Elements.German, "Firma");
company.PluralCaption.SetValue(Elements.English, "Companies");
company.PluralCaption.SetValue(Elements.German, "Firmen");
}
private void AddPerson()
{
var person = Elements.Classes.Person = Builder.AddClassWithDefaultPrimaryKey("Person", /*Guid*/, /*Guid*/);
Builder.AddInvisibleProperty(person, "CompanyId", MetaType.Key, true, /*Guid*/);
person.Caption.SetValue(Elements.English, "Person");
person.Caption.SetValue(Elements.German, "Person");
person.PluralCaption.SetValue(Elements.English, "People");
person.PluralCaption.SetValue(Elements.German, "Personen");
}
}
While this is a good technique for small models -- with anywhere up to five entities -- most models are larger and include entities with sizable metadata definitions. Another thing to consider is that, when working with larger teams, it's often best to keep a central item like the metadata definition as modular as possible.
To scale the pattern up for larger models, we can move code for larger entity definitions into separate generators. As soon as we move an entity to its own generator, we're faced with the question of where we should create paths for that entity. A path doesn't really belong to one class or another; in which generate should it go?
Well, we thought about that and came to the conclusion that the pattern should be to just create a separate generator for all paths in the model (or multiple path-only generators if you have a larger model). That is, when a model gets a bit larger, it should include the following generators (using the name "Demo" from the examples above):
DemoCoreGenerator
DemoPathGenerator
DemoCompanyGenerator
DemoPersonGenerator
The DemoCoreGenerator
will create metadata and assign elements like the display languages. It's also recommended to define base types like enumerations and very simple classes4 in the core as well. Obviously, as the model grows, the core generator may also get larger. This isn't a problem: just split the contents logically into multiple generators.
For the purposes of this example, though, we only have a single core and a single path generator and two entity generators. Since these generators will all be dependent on the model's builder and elements, the first step is to define a base class that will be used by the other generators.
internal class DemoDependentGenerator : DependentMetadataGeneratorBase<DemoModelGenerator, DemoModelElements, MetaBuilder>
{
}
public class DemoCoreGenerator : DemoDependentGenerator
{
protected override void AddCoreElements()
{
Elements.English = Builder.AddDisplayLanguage("en-US", "English");
Elements.German = Builder.AddDisplayLanguage("de-CH", "Deutsch");
}
}
public class DemoPathGenerator : DemoDependentGenerator
{
protected override void AddPaths()
{
Elements.Paths.CompanyPersonPath = Builder.AddOneToManyPath(
Elements.Classes.Company, "Id",
Elements.Classes.Person, "CompanyId",
/*Guid*/, /*Guid*/
);
}
}
public class DemoCompanyGenerator : DemoDependentGenerator
{
protected override void AddClasses()
{
var company = Elements.Classes.Company = Builder.AddClassWithDefaultPrimaryKey("Company", /*Guid*/, /*Guid*/);
company.Caption.SetValue(Elements.English, "Company");
company.Caption.SetValue(Elements.German, "Firma");
company.PluralCaption.SetValue(Elements.English, "Companies");
company.PluralCaption.SetValue(Elements.German, "Firmen");
}
protected override void AddProperties()
{
Builder.AddRelation(Elements.Classes.Person, "Company", "", Elements.Paths.CompanyPersonPath);
}
}
public class DemoPersonGenerator : DemoDependentGenerator
{
protected override void AddClasses()
{
var person = Elements.Classes.Person = Builder.AddClassWithDefaultPrimaryKey("Person", /*Guid*/, /*Guid*/);
Builder.AddInvisibleProperty(person, "CompanyId", MetaType.Key, true, /*Guid*/);
person.Caption.SetValue(Elements.English, "Person");
person.Caption.SetValue(Elements.German, "Person");
person.PluralCaption.SetValue(Elements.English, "People");
person.PluralCaption.SetValue(Elements.German, "Personen");
}
protected override void AddProperties()
{
Builder.AddRelation(Elements.Classes.Company, "People", "", Elements.Paths.CompanyPersonPath);
}
}
MetaBuilderBasedModelGeneratorBase<DemoModelElements>
{
protected override void AddMetadata()
{
Builder.CreateModel<DemoModel>("Demo", /*Guid*/);
Builder.CreateMainModule("Encodo.Quino");
Builder.Include<DemoCoreGenerator>();
Builder.Include<DemoPathGenerator>();
Builder.Include<DemoCompanyGenerator>();
Builder.Include<DemoPersonGenerator>();
}
}
You'll note that we only moved code around and didn't have to change any implementation or add any new elements or anything that might introduce subtle errors in the metadata. Please note, the classes are all shown in a single code block above, but the pattern dictates that each class should be in its own file.
So far, we've only worked with generators that are dependent on the model generator. How do we access information -- and elements -- generated in other modules? For example, let's include the security module and change a translation for a caption.
public class DemoModelElements
{
public DemoModelElements()
{
Classes = new DemoModelClasses();
Paths = new DemoModelPaths();
}
public ILanguage English { get; set; }
public ILanguage German { get; set; }
public SecurityModuleElements Security { get; set; }
public DemoModelClasses Classes { get; private set; }
public DemoModelPaths Paths { get; private set; }
}
public class DemoCoreGenerator : DemoDependentGenerator
{
protected override void AddCoreElements()
{
Elements.English = Builder.AddDisplayLanguage("en-US", "English");
Elements.German = Builder.AddDisplayLanguage("de-CH", "Deutsch");
Elements.Security = Builder.Include<SecurityModuleGenerator>().Elements;
}
protected override void AddProperties()
{
Elements.Security.Classes.User.Caption.SetValue(Elements.German, "Benutzer");
}
}
This approach works well with any module that has adhered to the pattern and exposes its elements in a standardized way.5 In this case, the core module includes the security module and retains a reference to its elements. Any code that uses the core module will now have access not only to the core elements but also to the security elements, as well.
Another major benefit to using this pattern is that the resulting code is quite self-explanatory: it's no mystery to what the Elements.Security.Classes.User.Caption
is referring.
The previous pattern had a single monolithic file. The new pattern increases the number of files -- possibly by quite a lot. It's recommended to put these new files into the following structure:
[-] Models
[+] Aspects
[+] Elements
[+] Generators
The "Aspects" folder isn't new to this pattern, but it's worth mentioning that any model-specific aspects should go into a separate folder.
That's all for now. Happy modeling!
Naturally, the IMetaModel
is always available and any part of the generation process can access metadata in the model at any time. However, the API for the model is quite generic and requires knowledge of the unique identifier or index for a piece of metadata.↩
By introspection, we mean that if metadata is accessed through .NET code structures -- like properties or constants -- we should be able to find all usages of a particular metadata element without resorting to a "find in files" for a particular string.↩
It doesn't have to go there. The DemoCoreGenerator
could also set up the builder (since it's using the same builder object). To do that, you'd override AddCoreElements()
and set up the model there. However, it's clearer to keep it in the generator that actually owns the builder that is being configured.↩
Simple classes generally have few extra properties and no layouts or short description classes.↩
Through the IElementProvider
mentioned above↩
The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.
The method WinformStatusFeedback.CreateStatusForm()
no longer exists and cannot be overridden. If you have code that looks like the following:
public class StartupFormFeedback : WinformDxStatusFeedback
{
public StartupFormFeedback(ICoreConfiguration configuration = null)
: base(configuration)
{ }
protected override IStatusForm CreateStatusForm()
{
return new StartupForm(Configuration);
}
}
you should replace it with the following:
public class StartupFormFeedback : WinformStatusFeedback
{
public StartupFormFeedback(ICoreConfiguration configuration = null)
: base(configuration, () => new StartupForm(configuration))
{ }
}
Please note that the base class in the example was changed from WinformStatusDxFeedback
to WinformStatusFeedback
(the former no longer allows the form to be overridden).
The summary below describes major new features, items of note and breaking changes. The full list of issues is also available for those with access to the Encodo issue tracker.
DocumentLibrary
and IContentRegistry
have been moved to the Documents
namespace.ReportsModule.DocumentLibrary
no longer exists; instead, access it via the service locator with ServiceLocator.Current.GetInstance<IDocumentLibrary>()
. Also the return type is an interface rather than the class itself, but the interface is unchanged.DocumentBase
constructor now takes an IDocumentLibraryProvider
instead of a DocumentLibraryProvider
DocumentLibraryProvider
is now called DocumentLibraryProviderBase
and the constructor no longer requires an IDocumentLibrary
.DocumentLibraryProvider
no longer has a DocumentLibrary
as a constructor parameter, it is no longer auto-registered. Instead, simply get the document library via the service locator (as shown above) and add it to the providers list manually.ViewClassAspect
no longer exists; instead, use ViewIconAspect
or ViewDynamicIconAspect
.Credentials
class is now abstract; instead, use UserCredentials
Encodo.Quino.Data.Persistence.Connections
no longer exists; simply remove the using
statement as it only included obsolete or internal classes and should have been removed in previous versions anyway.ViewContextHandler
now has generic parameters; instead, most usages will be able to simply switch to using the IViewContextHandler
interface.MetaBuilderBase.AddMethods
no longer accepts client, base and server classes as parameters. Instead, you should register methods via a single interface and register the appropriate implementation in the startup.For example, to register IDemoMethods, use:
builder.AddMethods<IBusinessLogicMethods>(...));
To set up a client to use the appropriate implementation, depending on whether a remoting data driver has been selected, use:
configuration.IntegrateRemotableMethods<IDemoMethods, LocalDemoMethods, RemotableDemoMethods>();
To set up the application server to host these methods, simply register the local version with the service locator, as shown below:
configuration.RegisterSingle<IRemoteMethods, ServerRemoteMethods>();
MetaMethodTools.Execute()
now requires four arguments because the call target must be included (remoting methods are no longer static).