„Renderer“ sind C#-Klassen, die sich um die visuelle Repräsentation eines Steuerelements bei der Verwendung in einer Maske kümmern. Sie definieren, ob der Benutzer beispielsweise ein Texteingabefeld oder ein Listenfeld sieht und auch, wie die vom Browser gesendeten Daten aufbereitet werden müssen, damit sie in das der Maske zugehörige Datenmodell gespeichert werden können.

Je nach Funktionsumfang muss die Basisklasse einer der folgenden sein

  • ControlDefaultRendererBase: Renderer OHNE Unterteilung in Read- und EditMode-Methoden und OHNE Bind-Unterstützung
  • ControlDefaultBindableRendererBase: Renderer OHNE Unterteilung in Read- und EditMode aber MIT Bind-Unterstützung
  • ControlDefaultReadEditRendererBase: Renderer MIT Unterteilung in Read- und EditMode-Methoden und OHNE Bind-Unterstützung
  • ControlDefaultReadEditBindableRendererBase: Renderer MIT Unterteilung in Read- und EditMode-Methoden und MIT Bind-Unterstützung

Die Zuordnung eines Renderers zu einem bestimmten Steuerelement erfolgt über ein entsprechendes Attribut:

[FormControlRenderer(typeof(AttachmentControl))]

Sie können durch die Angabe dieses Attributs in einem Projekt-Modul auch existierende Renderer übersteuern, indem Sie einen existierenden Renderer einfach neu definieren. Eine komplette neu Implementierung bespielsweise des TextEditControl ist zwar möglich aber nicht zu empfehlen.

Der Renderer beinhaltet im einfachsten falls nur die Methode Render() alternativ die beiden Methoden EditMode() und ReadMode(). Optional ist die Implementierung von Bind().

Die Render-Methoden sorgen für die korrekte Ansteuerung und Verwendung der visuellen Komponenten (bspw. DevExpress) für den entsprechenden Modus. Von den Möglichkeiten unterscheiden sich diese Methoden nicht. Der Entwickler ist dafür Verantwortlich in dem jeweiligen Modus, die entsprechenden Elemente für die UI zu generieren.

Die Bind-Methode sorgt dafür, dass die vom Browser gelieferten Daten korrekt in das Datenmodell der Maske geschrieben werden. Die Standardbehandlung von Bind() überträgt den Wert eines angezeigten Steuerelements in ein Feld. Dies kann unter drei Bedingungen automatisch durchgeführt werden.

  1. ViewState.ViewStateData muss unter dem Schlüssel BindKeys.ModelProperty den serverseitig verwendeten Feldnamen (Property-Namen auf dem BindObject) enhalten. Ist das Steuerlement ein DataControlBase, so wird dies vom Framework automatisch gemacht.
  2. ViewState.ViewStateData muss unter dem Schlüssel BindKeys.ClientProperty den clientseitig verwendeten Feldnamen (inkl. RenderParameter.NamePrefix) enthalten.
  3. Die ausgelesenen Daten, können ohne Typkonvertierung direkt in das Model geschrieben werden.

In der Bind-Methode hat man keinen Zugriff auf das Steuerelement selbst, daher müssen alle notwendigen Informationen für das Binden in ViewState.ViewStateData geschrieben werden. Dies können nur serialisierbare Daten sein.

Beispiel

Werfen wir zunächst einen Blick auf das Gerüst der Klasse, verwenden sie bitte auch hier einen Ihrem Projekt entsprechenden Namespace.

 namespace BA.Core.Configuration.Form.Renderers
{
    [FormControlRenderer(typeof(ValueAndAliasControl))]
    public class ValueAndAliasRenderer : ControlDefaultReadEditBindableRendererBase
    {
(… Klasseninhalt …)
    }
}

Die Klasse selbst erbt vom ControlDefaultReadEditBindableRendererBase und ist über das Attribut FormControlRenderer als Renderer für Steuerelemente des Typs ValueAndAliasControl gekennzeichnet.
Der „Klasseninhalt“ besteht folglich aus den Methoden EditMode(), ReadMode() und in unserem Beispiel auch Bind().

EditMode

Die Feldnamen mit den Suffixen „_Value“ und „_Alias“ werden von der EditMode() Methode festgelegt und in ViewStateData für die Bind() Methode hinterlegt. Die Methode verwendet eine private Hilfsmethode (CreateTextBoxItem), um das eigentliche DevExpress-Texteingabesteuerelement zu erstellen und zu parametrisieren. Auf diese Methode gehen wir noch im Anschluss ein. Die hier dargestellte oberste Ebene der Methode erstellt zwei Dev Express-Felder, deren Feldnamen jeweils dem Feldnamen des Datenmodells mit unterschiedlichen Suffixen entsprechen. Ein eventuell im Datenmodell verfügbarer Wert wird ausgelesen und der CreateControl-Methode als Vorbelegung übergeben. Nähere Beschreibungen bieten auch die Kommentarzeilen im Beispielquellcode.

public override void EditMode()
{
    ValueAndAliasControl valueAndAliasControl = (ValueAndAliasControl)RenderParameters.Control;

    // Feldnamen definieren (Datenmodell)
    string modelFieldName = valueAndAliasControl.OrmFieldName;
    ViewState.ViewStateData.AddOrUpdate(BindKeys.ModelProperty, modelFieldName);

    // Feldnamen definieren für die Felder im Browser definieren
    string uiValueFieldName = modelFieldName + "_Value";
    ViewState.ViewStateData.Add("ValueField", uiValueFieldName);
    string uiAliasFieldName = modelFieldName + "_Alias";
    ViewState.ViewStateData.Add("AliasField", uiAliasFieldName);

    // zu verwendendes Trennzeichen
    char delimiter = valueAndAliasControl.Delimiter.ToCharArray()[0];
    ViewState.ViewStateData.Add("Delimiter", delimiter);

    // Inhalt des gespeicherten Feldes holen und für die Felder vorbereiten
    string fieldContent = (string)ConvertFromModelPropertyType(GetValueFromModel());
    string[] splt = fieldContent?.Split(delimiter) ?? (new string[] { "", "" });
    string fieldContentPart1 = splt?[0] ?? "";
    string fieldContentPart2 = splt?.Length > 1 ? splt[1] : "";

    // Erstes Feld erzeugen und als Objekt hinzufügen
    MVCxFormLayoutItem mvcxItem = CreateTextBoxItem(valueAndAliasControl, uiValueFieldName, RenderParameters.RenderingContext.FormModel, "Wert", fieldContentPart1);
    RenderParameters.MvcxControls.Add(mvcxItem);

    // Zweites Feld erzeugen und als Objekt hinzufügen
    mvcxItem = CreateTextBoxItem(valueAndAliasControl, uiAliasFieldName, RenderParameters.RenderingContext.FormModel, "Alias", fieldContentPart2);
    RenderParameters.MvcxControls.Add(mvcxItem);
}

Konvention: Abruf der Werte in den Render-Methoden

Der Zugriff auf die Werte, muss in jedem Fall über die Deskriptoren erfolgen.

Für den Feldnamen des Steuerelementes auf dem Bind-Objekt für die Basisklasse PropertyControlBase und damit auch für DataSelection und DataControlBase wird garantiert, dass beim Abrufen von Daten zunächst GetValueFromModel() und anschließend ConvertFromModelPropertyType() aufgerufen wird.

Sollten weitere Werte aus dem Bind-Objekt benötigt werden, so werden diese ebenfalls immer unter Verwendung von PrepareBindProperty() und GetValueFromModel() abgerufen; der Aufruf auf ConvertFromModelPropertyType() ist in diesem Fall optional und muss von der Methode unterstützt werden.

Hieraus folgt:
Direkte Zugriffe auf RenderParameters.BindData sind immer kritisch zu betrachten. Hier ist man nur richtig, wenn tatsächlich der top-level-Datensatz verwendet werden soll. Das gültige Bind-Objekt für das aktuell angesteuerte Property ist BindObject! Was beispielsweise auch ein Teil-Datensatz sein kann.

Beispiel:
Der Feldname des Controls ist "Addresses[1].AddressType", BindData ist OrmCRMCompany.
Durch den Aufruf PrepareBindProperty("Addresses[1].AddressType") passiert folgendes:

  • RenderParameters.BindData bleibt wie es war eine Instanz von OrmCRMCompany
  • BindObject wird auf den angesteuerten Teildatensatz verschoben
  • PreparedProperty beinhaltet nun den PropertyDescriptor des Feldes "AddressType" für den Typen des Teildatensatzes

Bind()

Die Bind() Methode ist dafür zuständig, dass der Wert, der vom Request des Browsers, in das Model übertragen wird.

Die Funktion der Bind() Methode ist relativ einfach: Das Steuerelement zeigt in der Benutzeroberfläche zwei Felder, deren Namen aus ViewState.ViewStateData ausgelesen werden. Diese beiden Felder müssen aus den vom Browser kommenden Daten ausgelesen, zusammengerechnet und in das Model geschrieben werden. Beim Zusammenrechnen wird das konfigurierte Trennzeichen des Steuerelementes verwendet, welches ebenfalls aus ViewState.ViewStateData ausgelesen wird.

public override void Bind()
{
    // Auslesen der Daten von dem Value
    string bindValueFieldName_Value = (string)ViewState.ViewStateData["ValueField"];
    string value = (string)ConvertToModelPropertyType(GetValueFromRequest(bindValueFieldName_Value) ?? "");

    // Auslesen der Daten von dem Alias
    string bindValueFieldName_Alias = (string)ViewState.ViewStateData["AliasField"];
    string alias = (string)ConvertToModelPropertyType(GetValueFromRequest(bindValueFieldName_Alias) ?? "");

    // Berechnung des Wertes für das Model
    string delimiter = (string)ViewState.ViewStateData["Delimiter"];
    string modelValue = value + delimiter + alias;

    // Schreiben des Wertes in das Model
    WriteValueToModel(modelValue);
}

Konvention: Binden von Werten

Um eigene Werte zu binden muss zunächst GetValueFromRequest(), dann ConvertToModelPropertyType() und schließlich WriteValueToModel() aufgerufen werden.

CreateTextBoxItem

Die zuvor angesprochene Hilfsmethode CreateTextBoxItem() erstellt unter Zuhilfenahme der übergebenen Werte ein DevExpress-Steuerelement zur Texteingabe. Auch hier wurden erklärende Kommentare in den Quellcode eingefügt, um nähere Erläuterungen zu bestimmten Code-Teilen zu bieten.

private MVCxFormLayoutItem CreateTextBoxItem(ValueAndAliasControl valueAndAliasControl,
        string fieldName, DevExFormModel formModel, string label, string presetValue)
{
    MVCxFormLayoutItem mvcxItem = new MVCxFormLayoutItem();

    // Der Typ des neuen Controls => es ist eine "TextBox".
    mvcxItem.NestedExtensionType = FormLayoutNestedExtensionItemType.TextBox;

    // setzen verschiedener Einstellungen auf das MVCxItem, wie zB der Feldname, der im  
    // Webbrowser verwendet werden soll, die Beschriftung vor dem Feld, der Hilfetext, etc
    mvcxItem.Name = fieldName;
    mvcxItem.FieldName = fieldName;
    //mvcxItem.HelpText = RenderingUtils.GetHelpText(Api.Text.Format(valueAndAliasControl.HelpText));
    mvcxItem.ClientVisible = valueAndAliasControl.Visible;
    mvcxItem.VerticalAlign = FormLayoutVerticalAlign.Top;
    mvcxItem.Caption = label;
    mvcxItem.CaptionSettings.Location = Api.Enum.EnumValueConverter.GetLabelPosition(valueAndAliasControl.LabelPosition);
    mvcxItem.CaptionSettings.VerticalAlign = FormLayoutVerticalAlign.Top;

    // Weitere bestimmte Einstellungen sind im sog. "Settings"-Unterobjekt verfügbar.
    // Das betrifft zB die Editierbarkeit des Feldes, bestimmte Javascript-Events im Browser,
    // die ausgeführt werden sollen, Abmessungen des Feldes, usw.
    TextBoxSettings settings = (TextBoxSettings)mvcxItem.NestedExtensionSettings;

    // Horizontale Ausrichtung des Controls
    settings.ControlStyle.HorizontalAlign = HorizontalAlign.Left;

    // Ist das Feld eventuell für Bearbeitung gesperrt?
    if (!IsControlEditable())
    {
        settings.ClientEnabled = false;
        ViewState.ShouldBind = false;
    }

    // Breite des Feldes
    settings.Width = new Unit(valueAndAliasControl.Width, Api.Enum.EnumValueConverter.GetValueWidthType(valueAndAliasControl.WidthType));

    // Feld kann als fehlerhaft markiert werden, falls die Validierung fehlschlägt
    if (settings.Properties is EditProperties)
    {
        EditProperties props = (EditProperties)settings.Properties;
        ApplicationSubConfigurationForm appFormSettings = Api.Config.ApplicationConfiguration().GetSubConfiguration<ApplicationSubConfigurationForm>();

        props.ValidationSettings.Display = Api.Enum.EnumValueConverter.GetErrorFrameDisplayMode(appFormSettings.ErrorFrameDisplayMode);
        props.ValidationSettings.ErrorTextPosition = Api.Enum.EnumValueConverter.GetErrorTextPosition(appFormSettings.ErrorTextPosition);
        props.ValidationSettings.ErrorDisplayMode = Api.Enum.EnumValueConverter.GetErrorDisplayMode(appFormSettings.ErrorDisplayMode);
        settings.ShowModelErrors = true;
    }

    // Der "placeholder", also der Text, der in einem leeren Feld angezeigt werden soll
    settings.Properties.NullText = Api.Text.Format(valueAndAliasControl.NullText);

    // vorbelegen des übergebenen im Datenmodell schon verfügbaren Wertes
    if (!string.IsNullOrWhiteSpace(presetValue))
        settings.PreRender = (sender, arg) =>
        {
            MVCxTextBox txt = sender as MVCxTextBox;
            txt.Value = presetValue;
            txt.DataBind();
        };

    // spezieller Fix für FireFox, damit Text in Feldern selektiert und kopiert werden  
    // können, die keine Eingabe erlauben
    RenderingUtils.FixItemForFirefox(mvcxItem, settings, valueAndAliasControl.CssClass);
    return mvcxItem;
}

Read

In der Read-Methode hat man prinzipiell die identischen Möglichkeiten wie in der Edit-Methode. An dieser Stelle sollte man nur Elemente verwenden, die das Ändern der Daten nicht ermöglichen. In diesem Fall wird ein “Label” zur Anzeige verwendet.

public override void ReadMode()
{
    string namePrefix = RenderingUtils.GetUIReadyContextPrefix(RenderParameters.RenderingContext);
    ValueAndAliasControl valueAndAliasControl = (ValueAndAliasControl)RenderParameters.Control;

    // Feldnamen setzen
    string modelFieldName = valueAndAliasControl.OrmFieldName;

    // Alternativ kann ein MVCxFormLayoutItem auf diese Weise erstellt werden.
    RenderParameters.MvcxControls.Add(item =>
    {
        // Typ des Items definieren und Settings casten
        item.NestedExtensionType = FormLayoutNestedExtensionItemType.Label;
        LabelSettings labelSettings = (LabelSettings)item.NestedExtensionSettings;

        // Wert aus dem Datensatz auslesen und in als Inhalt festlegen
        char delimiter = valueAndAliasControl.Delimiter.ToCharArray()[0];
        string fieldContent = (string)ConvertFromModelPropertyType(GetValueFromModel());
        fieldContent = fieldContent?.Split(delimiter)?[0] ?? "";
        labelSettings.Text = fieldContent;

        // Weieter Einstellungen festlegen
        labelSettings.Name = namePrefix + valueAndAliasControl.ControlInternalName + RenderParameters.MvcxControls.Count;
        labelSettings.Width = new Unit(valueAndAliasControl.Width, Api.Enum.EnumValueConverter.GetValueWidthType(valueAndAliasControl.WidthType));
        labelSettings.ControlStyle.CssClass = valueAndAliasControl.CssClass;
        item.HorizontalAlign = FormLayoutHorizontalAlign.Left;
        item.Caption = valueAndAliasControl.Caption?.Translate() ?? string.Empty;
        item.CaptionSettings.Location = Api.Enum.EnumValueConverter.GetLabelPosition(valueAndAliasControl.LabelPosition);
        RenderingUtils.SetHelpText(valueAndAliasControl, item);
        item.Visible = valueAndAliasControl.Visible;

        // Label setzen, falls konfiguriert
        if (string.IsNullOrWhiteSpace(item.Caption))
            item.ShowCaption = DevExpress.Utils.DefaultBoolean.False;
    });
}