Note: This text is currently being split into several other topics.
Overview
This document covers the basics of the current Quino Builder API.
Architecture
The following assemblies are included in the Quino.Builders
assembly.
Encodo.Core
- most basic library for simple tasks.Encodo.Expressions
- basic expression handling.Quino.Meta
- standard meta definitions can be found here. Whenever you want to retrieve metadata from a model, a module, class or property you probably will found a matching extension in here. For details read the documentation.
In the Quino.Builders
assembly you'll find the following things:
Builders which are finalizing the model. They can help you with basics tasks like generating default layouts for every class. We're going to take a look at them later.
Extensions to create your model more quickly. Those methods are not doing any magic - they're there so you don't have to reinvent everything from scratch - which would be technically possible. We're going to look into the most frequently used once later on. The public API is documented and if you can't find an answer here you probably want to move on and search the extensions by yourself.
Application extensions which can be used by your application to setup your model at startup. We're going to take a look at those too later on.
All of the things mentioned above are supposed to help you setting up your model in a quick and clean way. Let's take a look at the basic definition of a model in the way Quino sees it. Every model has the following components:
The model itself is defined as an
IMetaModel
The model is divided into
IMetaModules
which are a logical way to split your model into different topics.Each module contains classes - this is where things are starting to get interesting.
A class consists of properties, relations and actions. For an in-deep overview of the Quino-Metdata concept consider reading the
Quino.Meta
documentation.
The general setup of a model is a straight forward process. We're using builders to create a model. Each builder provides access to the different stages of model creation. The following steps are currently available:
RegisterDependencies
use this method to include other builders. We're going to see an example later.AddCoreElements
is used to add low-level and independent components to the metadata (for example languages).AddClasses
is used to create the classes and foreign keys needed later on inAddPaths
.AddPaths
is used to establish paths between the classes. Foreign keys are created here too.AddProperties
is used to create any properties not needed for paths and to create relations (which are depending on paths).AddLayouts
gets called when all properties and relations are available. In this step we define the visual representation of our metadata.PostFinalize
can be used to finalize the model. Some of the internal builders of Quino use this step to create fallbacks in captions for example.
You can find some examples in the Sandbox.Core
assembly. In the next chapter we're going to setup a builder from scratch and define our model.
Basic usages
Create a simple model
In this chapter we're going to setup a simple builder and add a class and property to it.
Create a builder which derives from MetaBuilderBasedModelBuilderBase<TElements>
. The generic parameter TElements
can be used to pass data around with your builder. Fore example you probably want to make a class which contains all of your meta classes and the main module. This allows you to access them in a type-safe way without using the string identifier. For our sample builder we're going to create the following class and pass it as the generic parameter.
public class Elements { public IMetaModule Module { get; set; } public IMetaClass Company { get; set; } public IMetaClass Person { get; set; } }
Be aware that we need to set this properties in our builder. They're not set automatically for you. Like mentioned before - there's no magic.
Now lets create a builder which derives from MetaBuilderBasedModelBuilderBase<Elements>
. In our builder we're going to override the CreateModel
function to setup our custom model. The final builder should look something similar to this:
/// <inheritdoc/> public class SampleModelBuilder : MetaBuilderBasedModelBuilderBase<Elements> { protected override IMetaModel CreateModel() { Elements.Module = this.CreateModel("SampleModel").GetMainModule(); return Elements.Module.Model; } }
Don't create a new model in every builder. The model should only be created once. Otherwise you'll get an exception that the model has already been created. Use the
RegisterDependencies
method to include other builders. We're going to see an example on how to do that later.
Now let's add some content to our empty model. As a general pattern we recommend creating a separate builder for every class or object you're going to create. Now let's stick to this convention and create a CompanyBuilder
which derives from MetadataBuilderBase<TModelBuilder, TElements>
. We're going to pass in our MainModelBuilder
and the Elements
. In the next step we're going to let our MainModelBuilder
now about the new builder. To achieve that override the RegisterDependencies
in our MainModelBuilder
method and add the following code:
protected override void RegisterDependencies(IMetadataDependencies dependencies) { base.RegisterDependencies(dependencies); dependencies.Include<CompanyBuilder>(); }
In our CompanyBuilder
we're going to hook into the AddClasses
method to add our custom class. This should similar to this:
protected override void AddClasses() { Elements.Company = Elements.Module.AddClass("Company").AddId(); }
We're assigning our class to the property defined in our Elements
. The AddId()
call creates a default primary key on our class. Our graph would now look something similar to this:
SampleModel <- our model MainModule <- the default module Company <- our class Id <- our default primary key
Now we're going to add some properties to the Company
class. Override the AddProperties
method and add the following code:
protected override AddProperties() { Elements.Company.AddProperty("Firstname", MetaType.Text); }
We now want to create a third builder and create a Person
class with it. We can create the builder and set up the class similar to the one we already did. Keep in mind that you need to link them together in the MainModelBuilder
.
In this chapter we've created a simple model which consists of two classes. In the next chapter we're going to look into the task of creating paths and relations.
It's all about relations
In the previous chapter, we set up two builders with two classes. We're now going to create a relation between them.
Let's override the AddPaths
method in our `CompanyBuilder``` and add the following code:
protected override AddPaths() { Elements.CompanyPersonPath = Model.AddPath( Elements.Company.FromOne(), Elements.Person.ToMany("CompanyId") ); }
We're setting up a path for the database. It's a 1:n relation between Company and Person. Next we're going to setup some navigation properties for easier access. You can use the AddProperties()
method to do so. Let's add the company to our PersonBuilder
:
protected override AddProperties() { Elements.Classes.Person.AddRelation("Company", Elements.Paths.CompanyPersonPath); }
Now our relation is set up and we can navigate from the Person to the assigned Company. All other relations can be set up similarly.
Virtual relations
You need to generate code/metadata for your model in order for this sample to compile.
You can define virtual relations that are not based on a path and that don't even need to be mapped to the database. Since we don't have a path, we have to tell Quino how to load data for the relation.
The first step is to implement IQueryAspect
. The sample below shows several new things:
private class LastNameQueryAspect : QueryAspectBase { /// <inheritdoc/> public override void SetUpQuery(IDataSession session, IQuery query) { query.WhereEquals(SampleModel.Person.LastName, ""Müller""); } }
Apply calls to the
query
to restrict, sort, etc.Use the
session
to get data from other sources (e.g. through other relations)Use the generated
SampleModel
metadata to reference fields and tables in a type-safe manner
After that, add the virtual relation and include you aspect on it.
var virtualRelation = Model.AddVirtualRelation( Elements.Company.FromOneVirtual(), Elements.Person.ToManyVirtual() ).SetAspect(new LastNameQueryAspect());
This can be handy if you have to access the same sort of data multiple times in your application. This way you can actually define a property in your class which contains exactly the related list you need, even if the query to get that data is more complex.
Delegate Properties
You need to generate code/metadata for your model in order for this sample to compile.
For example we now want to create a property which returns the name of the company currently assigned to the person. For that we extend the AddProperties()
method on the PersonBuilder
. It should look something similar to this:
Elements.Classes.Person.AddDelegateProperty<Person>("CompanyName", MetaType.Text, person => person.Company.Name);
We aware that this sort of expression can be dangerous.
Multi-language Properties
// TODO
Layouts
Layouts define how your classes are displayed to the end user. The basic concept is simple:
Every
IMetaClass
has a collection of Layouts. All layouts are of typeIMetaGroup
and can contain other meta elements.There are four general types of layouts
The detail layout defines how your class is shown to the user and which fields are visible.
The list layout defines how your class looks if showed in list or data grid.
The title layout defines how your class is displayed as a string.
The search layout defines which properties are contributing to the search.
For most cases the default layout setup is enough to fulfill your needs. We're going to look at an advanced use case later on.
Let's create a simple Layout for a class.
protected override AddLayouts() { var companyMetaClass = Elements.Company; using(var layout = companyMetaClass.CreateDetailLayout(ControlType.Group, "CaptionForCompany")) { layout.Add(companyMetaClass["Name"], companyMetaClass["Street"], companyMetaClass["ZipCode"], companyMetaClass["City"]); } using(var layout = companyMetaClass.CreateListLayout()) { layout.Add(companyMetaClass["Name"]); } using(var layout = companyMetaClass.CreateTitleLayout()) { layout.Add(companyMetaClass["Name"]); } }
The Layouts are used with the "using" clause which allows automatic stacking. Let's create a nested layout for the detail.
protected override AddLayouts() { var companyMetaClass = Elements.Company; using(var layout = companyMetaClass.CreateDetailLayout(ControlType.TabContainer, "CaptionForCompany")) { using(layout.AddTab("Name")) { layout.Add(companyMetaClass["Name"]); } using(layout.AddTab("Street and City")) { layout.Add(companyMetaClass["Street"], companyMetaClass["ZipCode"], companyMetaClass["City"]); } } // ----- snip ----- }
Our detail layout now contains a tab control with two tab items. The caption is provided in the AddTab
method. This way we can nest our layouts and add different containers on the same page.
If you want to add a menu structure to your model you can attach it with the following pattern:
protected override AddLayouts() { var companyMetaClass = Elements.Company; var mainMenu = companyMetaClass.Model.CreateMainMenuGroup("MyMenuGroup"); mainMenu.CreateMenuEntry(companyMetaClass, "MyMenuCompanyEntry"); // ----- snip ----- }
This will create a menu group which consists of a single item. The item references to the company meta class and will therefore open the default list layout for the company if clicked. If you want to stack your menu structure further you're allowed to do so. The following code shows an example of a stacked menu group:
protected override AddLayouts() { var companyMetaClass = Elements.Company; var mainMenu = companyMetaClass.Model.CreateMainMenuGroup("MyMenuGroup"); var subgroup = mainMenu.CreateMenuGroup("MySubgroup"); subgroup.CreateMenuEntry(companyMetaClass, "MyMenuCompanyEntry"); subgroup.CreateMenuEntry(companyMetaClass, "MyMenuCompanyEntry2"); // ----- snip ----- }
This way our menu contains the following structure:
- MyMenuGroup - MySubgroup -MyMenuCompanyEntry -MyMenuCompanyEntry2
If you want to refer to a custom layout you need to set the right identifier which is matching with your custom layouts identifier. The CreateMenuEntry
has an optional parameter which can be used for this. The custom layouts must all be named the same. The framework is handling the difference between list, detail, title and search.
Fluent API Overview
The fluent builder API is builded up on five files. Whenever you search for some function those files are probably a good starting point.
ModelExtensions
contains all the extension which target the module itself. For example creating a new module.ModuleExtensions
contains all the functions to create classes and add them to the specific module.ClassExtensions
contains all the functions to manipulate classes and adding properties and relations.PropertyExtensions
are used to manipulate properties.LayoutExtensions
can be used for layouts. They contain all the crucial methods for generating and manipulating layouts.
The extension methods do not contain any magic. They're basically designed to help you write code faster. And they don't force you to repeat yourself all the time. Some of the most important shall be listed here:
CreateModule(this IMetaModel model, string identifier)
create a new module and attach it to the model.AddClass(this IMetaModule module, string identifier)
create a new class for the specified module.AddId(this IMetaClass metaClass)
can be used to create and assign a default Id-property to your class.AddProperty(this IMetaClass metaClass, string identifier, MetaType type)
add a new property to your class with the defined type.AddCalculatedProperty(this IMetaClass metaClass, string identifier, MetaType type, IExpressionParser expressionParser, string expression)
add a new calculated property which evaluates to the given expression. For an overview about expressions see theEncodo.Expressions
assembly documentation.