Skip to content

Create a control

BSick7 edited this page Jan 23, 2015 · 16 revisions

This guide should help developers understand the role and inner workings of controls. Complain to us if it doesn't!

Purpose

Why would a developer build a control? A control is built to extend the functionality of XAML. This is usually done through a composition of Fayde elements, panels, and/or other controls. Controls should be agnostic to business problems and target shortfalls of the existing view functionality. In other frameworks, the concept of controls has been used as a way to reuse code or change the look and feel of an existing control. This is highly discouraged as there are better alternatives. To better fit a UX plan, use styles and templates to theme the control.

Control Responsibilities

  • Templating
  • Template
  • Background
  • Foreground
  • BorderBrush
  • BorderThickness
  • FontSize
  • FontFamily
  • FontStretch
  • FontStyle
  • FontWeight
  • Horizontal Alignment
  • Vertical Alignment
  • Padding
  • Extended User Interaction
  • IsFocused
  • IsEnabled
  • IsTabStop
  • TabIndex
  • TabNavigation

Getting started

A control is usually composed of 2 pieces: view and code. Even if the control is a composition of other elements, the view still contains a template. For this guide, I will assume that a [controls library](Create a controls library) has been created. The view resides in the theme file as an implicit style and the code resides in a Typescript file labelled with the controls' namesake. Note the "DefaultStyleKey" definition in the constructor is critical to hooking up the implicit style.

Library: ControlsLibrary, Control: Widget

Default.theme.xml

<ResourceDictionary
    xmlns="http://schemas.wsick.com/fayde"
    xmlns:x="http://schemas.wsick.com/fayde/x"
    xmlns:controls="lib://ControlsLibrary">
    <Style TargetType="controls:Widget">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="controls:Widget">
                    <!-- Insert template -->
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
    <!-- Other styles... -->
</ResourceDictionary>

Widget.ts

module ControlsLibrary {
    import Control = Fayde.Controls.Control;
    export class Widget extends Control {
        constructor() {
            super();
            this.DefaultStyleKey = Widget;
        }
        //code...
    }
}

Visual States

Reacting to data changes or user input is accomplished through the visual state manager. A control is usually composed of visual state groups each of which contain visual states. A visual state is composed of storyboards which define how the view should transmute when that visual state is active. (Only 1 visual state is active in a visual state group) The following illustrates how a Button's visual state is declared in its theme.

...
<ControlTemplate TargetType="Button">
	<Grid>
		<VisualStateManager.VisualStateGroups>
			<VisualStateGroup x:Name="CommonStates">
				<VisualState x:Name="Normal"/>
				<VisualState x:Name="MouseOver">
					<Storyboard>
						<DoubleAnimation Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="MouseOverBorder" To="1" Duration="0" />
					</Storyboard>
				</VisualState>
				<VisualState x:Name="Pressed">
					<Storyboard>
						<DoubleAnimation Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="PressedBorder" To="1" Duration="0" />
					</Storyboard>
				</VisualState>
				<VisualState x:Name="Disabled">
					<Storyboard>
						<DoubleAnimation Storyboard.TargetProperty="Opacity" Storyboard.TargetName="DisabledVisualElement" To="0.7" Duration="0" />
						<DoubleAnimation Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="contentPresenter" To="0.3" Duration="0" />
					</Storyboard>
				</VisualState>
			</VisualStateGroup>
			<VisualStateGroup x:Name="FocusStates">
				<VisualState x:Name="Focused">
					<Storyboard>
						<DoubleAnimation Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="FocusRectangle" To="1" Duration="0" />
						<DoubleAnimation Storyboard.TargetProperty="(UIElement.Opacity)" Storyboard.TargetName="FocusInnerRectangle" To="1" Duration="0" />
					</Storyboard>
				</VisualState>
				<VisualState x:Name="Unfocused"/>
			</VisualStateGroup>
		</VisualStateManager.VisualStateGroups>
		<Border x:Name="Background" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" CornerRadius="3"/>
		<Rectangle x:Name="DisabledVisualElement" Fill="{StaticResource ControlsDisabledBrush}" IsHitTestVisible="false" Opacity="0" RadiusY="3" RadiusX="3"/>
		<Border x:Name="MouseOverBorder" Background="{StaticResource GrayBrush8}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3.5" Opacity="0"/>
		<Border x:Name="PressedBorder" Background="{StaticResource GrayBrush5}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="3.5" Opacity="0"/>
		<Rectangle x:Name="FocusRectangle" Stroke="{StaticResource TextBoxMouseOverInnerBorderBrush}" RadiusY="4" RadiusX="4" Margin="-1" Opacity="0" />
		<Rectangle x:Name="FocusInnerRectangle" StrokeThickness="{TemplateBinding BorderThickness}" Stroke="{StaticResource TextBoxMouseOverBorderBrush}" RadiusX="3" RadiusY="3" Opacity="0" />
		<ContentPresenter x:Name="contentPresenter" ContentTemplate="{TemplateBinding ContentTemplate}" Content="{TemplateBinding Content}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
	</Grid>
</ControlTemplate>
...

Visual states must be activated manually within the owning control. There is a feature within Fayde that makes this process easier than Silverlight. Here are the pertinent declarations within Control:

UpdateVisualState(useTransitions?: boolean): void;
GoToStates(gotoFunc: (state: string) => boolean): void;
GoToStateCommon(gotoFunc: (state: string) => boolean): boolean;
GoToStateFocus(gotoFunc: (state: string) => boolean): boolean;
GoToStateSelection(gotoFunc: (state: string) => boolean): boolean;

To update the view, call this.UpdateVisualState();. By default, Control.GoToStateCommon will activate Normal, MouseOver, or Disabled depending the control's state. These can be overriden like the Button's code:

GoToStateCommon(gotoFunc: (state: string) => boolean): boolean {
    if (!this.IsEnabled)
        return gotoFunc("Disabled");
    if (this.IsPressed)
        return gotoFunc("Pressed");
    if (this.IsMouseOver)
        return gotoFunc("MouseOver");
    return gotoFunc("Normal");
}

If a new visual state group is added, a new 'GoToState' will be needed. Here is an example of a TreeViewItem adding visual states. Visual state names must be unique across all groups.

GoToStates(gotoFunc: (state: string) => boolean) {
    gotoFunc(this.IsExpanded ? "Expanded" : "Collapsed");
    gotoFunc(this.HasItems ? "HasItems" : "NoItems");
    if (this.IsSelected)
        gotoFunc(this.IsSelectionActive ? "Selected" : "SelectedInactive");
    else
        gotoFunc("Unselected");
}

When creating a control with visual states, it is recommended that metadata is added so other developers can query the defined contract (and for future support for a designer). Here is the definition for TreeViewItem with noise redacted.

export class TreeViewItem extends HeaderedItemsControl {
    //...
}
Fayde.Controls.TemplateVisualStates(TreeViewItem,
    { GroupName: "CommonStates", Name: "Normal" },
    { GroupName: "CommonStates", Name: "MouseOver" },
    { GroupName: "CommonStates", Name: "Pressed" },
    { GroupName: "CommonStates", Name: "Disabled" },
    { GroupName: "FocusStates", Name: "Unfocused" },
    { GroupName: "FocusStates", Name: "Focused" },
    { GroupName: "ExpansionStates", Name: "Collapsed" },
    { GroupName: "ExpansionStates", Name: "Expanded" },
    { GroupName: "HasItemsStates", Name: "HasItems" },
    { GroupName: "HasItemsStates", Name: "NoItems" },
    { GroupName: "SelectionStates", Name: "Unselected" },
    { GroupName: "SelectionStates", Name: "Selected" },
    { GroupName: "SelectionStates", Name: "SelectedInactive" });

To query this data, a developer could open the browser console and type Fayde.Controls.TemplateVisualStates.Get(Fayde.Controls.TreeViewItem)

Template Parts

In many scenarios, you need to acquire a visual element declared in your view to mutate in your code. These template parts are discovered in OnApplyTemplate and metadata should be defined very similar to TemplateVisualStates. Here is the definition for TreeViewItem with noise redacted.

export class TreeViewItem extends HeaderedItemsControl {
    private _Header: FrameworkElement = null;
    private _ExpanderButton: Primitives.ToggleButton = null;

    OnApplyTemplate() {
        super.OnApplyTemplate();
        this._ExpanderButton = <Primitives.ToggleButton>this.GetTemplateChild("ExpanderButton", Primitives.ToggleButton);
        this._HeaderElement = <FrameworkElement>this.GetTemplateChild("Header", FrameworkElement);
        this.UpdateVisualState();
    }
}
TemplateParts(TreeViewItem,
    { Name: "Header", Type: FrameworkElement },
    { Name: "ExpanderButton", Type: Primitives.ToggleButton });