In This Topic
Themes Overview
When it comes to styling flexibility CSS is perfect (see Styling and Inheritance). However the authoring of large amounts of CSS, that is in the same optimized and non-redundant is a sophisticated task. Web authors fail to produce quality large CSS even for the small amount of HTML elements, and NOV has hundreds of elements that need to be styled. At help come NOV themes.
NOV themes are factories for NStyleSheet instances and are represented by the NTheme abstract class. The purpose of themes is to facilitate the authoring of stylesheets that apply styling to a specific domain of elements - (User Interface Elements are for example styled with instances of the Nevron.UI.NUITheme class). The NTheme-CreateStyleSheet() returns a style sheet that is automatically generated by the theme:
Creating a Theme Stylesheet |
Copy Code
|
NTheme theme = new SomeThemeType();
NStyleSheet styleSheet = theme.CreateStyleSheet();
|
The stylesheet produced by a specific theme type needs to be applied to some NDocument instance in order to be effective. Because that document may already have a stylesheet produced by the same theme type, the previously existing sheet, created by the respective theme type needs to first be removed. This is simplified by the NStyleSheetCollection-ApplyTheme method, which does that for you. So in most cases you will use themes like this:
Applying a Theme to a Document |
Copy Code
|
document.StyleSheets.ApplyTheme(new SomeThemeType());
|
To achieve simplification in the authoring of complex stylesheets, themes first define the notion of context and state parts of a selector. The state part is used to represent a collection of conditions, while the context part is used to represent the rest of the selector - i.e. combinators and conditions. The states and contexts of a particular theme are typically created in its constructor and are reused during the stylesheet authoring process.
When a theme is asked to create a new NStyleSheet instance, it internally calls the NTheme-CreateTheme() protected abstract method. This method should be overriden in derived classes to create the theme rules and skins.
Theme rules are used to create a single NRule instance. Theme rules are obtained by calls to the NTheme-GetRule methods, that take as argument the base schema of the elements targeted by the rule, followed by an arbitrary sequence of states and contexts.
Theme skins are used to produce multiple NRule instances - one for each skin style. Therefore a theme skin is a group of rules that apply to an element of some type. Theme skins are obtained by calls the NTheme-GetSkin methods, that take as argument the base schema of the elements targeted by the skin. Skin styles are obtained by calls to the NThemeSkin-GetStyle method that takes an arbitrary sequence of states and contexts.
Theme rules are represented by the NThemeRule class (derived from the NThemeStyle class). Theme skins are represented by the NThemeSkin class (derives from the NThemeStyle class). Theme skin styles are represented by instances of NThemeStyle class. The NThemeStyle class is used to represent the declaration of a rule, and is simply a map between a DOM property and its value.
My First Theme
For the purpose of demonstrating themes suppose that you need to author a stylesheet that styles the background of Nevron.UI.NButton instances, depending on the following criterias:
1. The buttons should by default have green background.
2. When the mouse is over buttons, the buttons should have red background.
3. If a button is inside a NTabPage, the button should have a blue background.
4. If a button is inside a NTabPage and the mouse is over the button, the button should have a yellow background.
From the sample criterias definition, we can see that we need one state (whether the mouse is over the button) and one context (whether the button is inside a NTabPage). Lets now see how this theme will look in code, if implemented via rules:
My First Theme |
Copy Code
|
public class MyFirstTheme : NTheme
{
public MyFirstTheme()
{
// create the theme states and contexts
IsMouseOverState = CreateState(delegate(NSelectorBuilder sb) { sb.ValueEquals(NMouse.IsOverPropertyEx, true); });
InTabPageContext = CreateContext(delegate(NSelectorBuilder sb) { sb.DescendantOf(); sb.Type(NTabPage.NTabPageSchema); });
}
public override string GetThemeType()
{
return "MyFirstTheme";
}
protected override void CreateTheme()
{
// 1. The buttons should by default have green background.
NThemeRule defaultRule = GetRule(NButton.NButtonSchema);
defaultRule.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Green));
// 2. When the mouse is over buttons, the buttons should have red background.
NThemeRule mouseOverRule = GetRule(NButton.NButtonSchema, IsMouseOverState);
mouseOverRule.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Red));
// 3. If a button is inside a NTabPage, the button should have a blue background.
NThemeRule inTabPageRule = GetRule(NButton.NButtonSchema, InTabPageContext);
inTabPageRule.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Blue));
// 4. If a button is inside a NTabPage and the mouse is over the button, the button should have a yellow background.
NThemeRule inTabPageMouseOverRule = GetRule(NButton.NButtonSchema, IsMouseOverState, InTabPageContext);
inTabPageMouseOverRule.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Yellow));
}
// state and context fields
public readonly NThemingState IsMouseOverState;
public readonly NThemingContext InTabPageContext;
}
|
From the rules authoring approach we see that the each GetRule call starts with a NButton.NButtonSchema reference - the base schema of the elements targeted by the rule. This is because each rule in this example represents a specific style of the button. But a group of styles that applies to a single type of element is actually a skin, hence we can compact this theme even more if we use skins:
My First Skin |
Copy Code
|
protected override void CreateTheme()
{
NThemeSkin buttonSkin = GetSkin(NButton.NButtonSchema);
// 1. The buttons should by default have green background.
buttonSkin.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Green));
// 2. When the mouse is over buttons, the buttons should have red background.
NThemeStyle mouseOverStyle = buttonSkin.GetStyle(IsMouseOverState);
mouseOverStyle.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Red));
// 3. If a button is inside a NTabPage, the button should have a blue background.
NThemeStyle inTabPageStyle = buttonSkin.GetStyle(InTabPageContext);
inTabPageStyle.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Blue));
// 4. If a button is inside a NTabPage and the mouse is over the button, the button should have a yellow background.
NThemeStyle inTabPageMouseOverStyle = buttonSkin.GetStyle(IsMouseOverState, InTabPageContext);
inTabPageMouseOverStyle.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Yellow));
}
|
Theme States and Theme Contexts
The purpose of states and contexts is to reduce the code required to produce selectors for rules and skins.
States are represented by instances of the NThemingState class (derived from NThemeSelectorPart). States are created via calls to the NTheme-CreateState method, that need to take a valid delegate that emits conditions to a selector builder. This means that states are created for single or multiple selector conditions that do not change the target for matching.
Contexts are represented by instances of the NThemeContext class (derived from NThemeSelectorPart). Contexts are created via calls to the NTheme-CreateContext method, that need to take a valid delegate that emits combinators and conditions to a selector builder. This means that contexts are created when a change to the target for matching is required.
Any selector can be constructed by coupling states and contexts, in arbitrary order. Because NThemeState and NThemeContext both derive from the NThemeSelectorPart abstract class, the NTheme-GetRule method and the NSkin-GetStyle method are accepting an arbitrary array of NThemeSelectorPart instances (i.e. an arbitrary ordered set of states and contexts - selector parts).
Theme Rules
Theme rules are represented by instances of the NThemeRule class, that derives from the base NThemeStyle class. Each theme rule creates a single NRule instance in the NStyleSheet created by the theme. The created rule is having only one selector - the selector produced by the theme rule. The declaration of the NRule contains the entries that are defined by the NThemeStyle from which the NThemeRule derives.
NThemeRule instances are obtained from the NTheme-GetRule methods. All GetRule overloads start with the base schema of the elements targeted by the rule. The selector produced by the rule is prefixed with the base schema, with which the rule was obtained. To better understand the actual CSS generated by theme rules, we will rewrite the fourth criteria of the My First Theme example with raw CSS. Here is how it looked with themes:
Button in Tab Page rule with Themes |
Copy Code
|
// 4. If a button is inside a NTabPage and the mouse is over the button, the button should have a yellow background.
NThemeRule inTabPageMouseOverRule = GetRule(NButton.NButtonSchema, IsMouseOverState, InTabPageContext);
inTabPageMouseOverRule.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Yellow));
|
And here is how it looks with raw CSS:
Button in Tab Page rule with raw CSS |
Copy Code
|
// 4. If a button is inside a NTabPage and the mouse is over the button, the button should have a yellow background.
NRule rule = new NRule();
NSelectorBuilder sb = rule.GetSelectorBuilder();
sb.Start();
// this is emitted by NThemeRule
sb.Type(NButton.NButtonSchema);
// this is emitted by IsMouseOverState
sb.ValueEquals(NMouse.IsOverPropertyEx, true);
// this is emitted by InTabPageContext
sb.DescendantOf();
sb.Type(NTabPage.NTabPageSchema);
sb.End();
// this is emitted by the base NThemeStyle
rule.Declarations.Add(new NValueDeclaration<NFill>(NButton.BackgroundFillProperty, new NColorFill(NColor.Yellow)));
|
Theme Skins
Theme skins are represented by instances of the NThemeSkin class, that derives from the base NThemeStyle class. A theme skin represents a collection of rules that apply to an element of a certain base schema. Each theme skin produces the following rules:
1. Default State Rule – this rule is produced by the theme skin itself – it aims to apply default values to elements that are instances of the base schema.
2. Skin Style Rules – a single rule is produced by each style of the skin. Skin styles are obtained by the NThemeSkin-GetStyle method, which accepts an arbitrary sequence of states and contexts.
NThemeSkin instances are obtained from the NTheme-GetSkin methods. All GetSkin overloads start with the base schema of the elements targeted by the skin.
There are two reasons for creating skins over rules:
1. To group the rules related to a particular type of elements.
2. To allow Skin Overriding.
Skin overriding is automatically performed by the theme. Suppose that you have three types of elements – A, B and C, where B and C derive from A. If you want to skin all elements that are instances of type A (A, B and C), you can create a skin that targets the A base schema. If you want to skin element B in a different way, you need to also create a skin that targets the B base schema. The presence of a B schema skin, implicitly instructs the theme to prefix the rules created by the A skin to exclude elements of type B from the set of matching elements.
To better illustrate the raw CSS emitted by skins suppose that you have created a MyButton class that derives from the NButton class and you want that specific button to always be with magenta background. Here is how it will look with themes:
Skin Overriding with Themes. |
Copy Code
|
// NButton skin from My First Skin example here.
NThemeSkin myButtonSkin = GetSkin(MyButton.MyButtonSchema);
myButtonSkin.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Magenta));
|
Nothing special in that code, but what happens with the actually generated rules of the NButton skin, is that they are automatically prefixed to exclude instances of MyButton like this:
Raw Selector Prefixing produced by Skin Overriding. |
Copy Code
|
NRule rule = new NRule();
NSelectorBuilder sb = rule.GetSelectorBuilder();
sb.Start();
// this is prefix of all rules emitted by the NButton skin.
// it aims to exclude buttons of type MyButton, without altering the specificity of the produced selector.
sb.Type(NButton.NButtonSchema);
sb.StartInvertedConditions();
sb.Type(MyButton.MyButtonSchema).SpecificityRank = ENConditionSpecificityRank.None;
sb.EndInvertedConditions();
|
The Skin Overriding concept is introduced because of the polymorphic environment in which NOV CSS needs to operate. In HTML things related to the type selector are simple, because HTML tags are not polymorphic and you cannot define new tags based on already defined tags. This is just one of the areas in which NOV CSS exceeds the capabilities of the CSS present in HTML – the polymorphic type condition.
A derivate concept of the Skin Overriding is concept of a Skin Type. A skin type lets you group the rules that apply to a specific base schema AND skin type. It lets you implement the following sample polymorphic requirement:
Keep My First Example criterias 1 and 2 for all NButton instances (in our case NButton and MyButton)
Keep My First Example criterias 3 and 4 for any NButton instances except MyButton instances.
With skins this would look like:
Skin Type Example |
Copy Code
|
protected override void CreateTheme()
{
NThemeSkin buttonSkin = GetSkin(NButton.NButtonSchema);
object inTabPageType = new object();
NThemeSkin buttonInTabPageSkin = GetSkin(NButton.NButtonSchema, inTabPageType);
// 1. The buttons should by default have green background.
buttonSkin.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Green));
// 2. When the mouse is over buttons, the buttons should have red background.
NThemeStyle mouseOverStyle = buttonSkin.GetStyle(IsMouseOverState);
mouseOverStyle.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Red));
// 3. (inTabPageType) If a button is inside a NTabPage, the button should have a blue background.
NThemeStyle inTabPageStyle = buttonInTabPageSkin.GetStyle(InTabPageContext);
inTabPageStyle.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Blue));
// 4. (inTabPageType) If a button is inside a NTabPage and the mouse is over the button, the button should have a yellow background.
NThemeStyle inTabPageMouseOverStyle = buttonInTabPageSkin.GetStyle(IsMouseOverState, InTabPageContext);
inTabPageMouseOverStyle.Set(NButton.BackgroundFillProperty, new NColorFill(NColor.Yellow));
// for instances of my button -> simply exclude criterias 3 and 4 by overriding the inTabPageType skin type for MyButton instances.
NThemeSkin myButtonSkin = GetSkin(MyButton.MyButtonSchema, inTabPageType);
}
|
Advanced Features
This section discusses some advanced features of NOV themes that will help you use them more efficiently and also help you understand how NOV themes help you generate quality CSS. In order to understand them you need to be familiar with NOV CSS property cascade, which is described in the CSS Styling and Inheritance topic.
Reduced Declaration Redundancy
This feature is needed, because in a CSS declaration you can add two value declarations that apply different or the same value to the same DOM property. CSS however takes into account only the last declaration for a specific DOM property. Hence the NThemeStyle class is implemented as a map - holds only one value for a specific DOM property.
Reduced Rules Redundancy
This feature is needed, because in CSS you can create two rules that have the same selector, and the two rules may contain a declaration for the same DOM property. That is why in themes you get theme rules by calling the NTheme-GetRule, which requires you to specify the full selector of the rule (via base schema and parts). If a theme rule with the same selector was already created the theme will return that rule. Because NThemeRule derives from NThemeStyle it will not allow you to make a redundant declaration.
Auto Rules Reordering
When CSS selectors have different specificity - everything is fine, since rules order does not matter. However when CSS selectors have the same specificity, the last declaration wins. Themes battle this CSS ambiguity by auto reordering the rules generated by the user and by skins via the following rules:
1. Rules are first sorted by the respective base schema depth from root – i.e. rules for higher level element types will appear after rules for lower level element types. This aims to give priority to rules that apply to elements that are deeper in the elements type hierarchy.
2. Rules are then sorted by their selector parts declaration order. The selector parts for the rules are compared one by one in first-to-last order. If the selector part from the first selector is declared before the selector part of the second selector - the rule for the first selector will appear after the rule for the second selector. This aims to give priority to rules that begin with selector parts that are declared first.
See Also