Markup controls with code-behind

Hi guys
Iā€™m trying to make a custom control. I chose to make the control as a Markup control (DotvvmMarkupControl) so that I donā€™t have to create the HTML structure of the control with C# code as it would be with a Code-only control, but the result should behave like a Code-only control.
I have a few issues.

The first is that I donā€™t want to have any viewmodel here (like in the Code-only control), the properties will be bind from the viewmodel when the control wil be used in application.
@viewmodel is required here, but I got around that using:
@viewModel System.Object
but itā€™s not completely clean solution.

The second thing is about properties. I would need to make a derived property, I would like to do something like:

public string MessageTypeClass => this.MessageType switch
{
    MessageType.Info => "alert-info",
    MessageType.Warning => "alert-warning",
    _ => "alert-danger",
};

But in this case property MessageTypeClass isnā€™t working.
So I instead of this do a complete DotvvmProperty and set its value in OnPreRender method. This partially works, but after postback is sometimes property not updated correctly.

My code is this:
dotcontrol:

@viewModel System.Object
@baseType DC.HodnoceniZamestnancu.Controls.MessageAlert, DC.HodnoceniZamestnancu

{{value: _control.MessageType}}
{{value: _control.MessageTypeClass}}

and code behind:

public class MessageAlert : DotvvmMarkupControl
{
    public static readonly DotvvmProperty MessageTypeProperty = DotvvmProperty.Register<MessageType, MessageAlert>(c => c.MessageType, MessageType.Error);
    public static readonly DotvvmProperty MessageTypeClassProperty = DotvvmProperty.Register<string, MessageAlert>(c => c.MessageTypeClass, "alert-danger");

    public MessageType MessageType
    {
        get { return (MessageType)GetValue(MessageTypeProperty)!; }
        set { SetValue(MessageTypeProperty, value); }
    }

    public string MessageTypeClass
    {
        get { return (string)GetValue(MessageTypeClassProperty)!; }
        set { SetValue(MessageTypeClassProperty, value); }
    }

    protected override void OnPreRender(IDotvvmRequestContext context)
    {
        this.MessageTypeClass = this.MessageType switch
        {
            MessageType.Info => "alert-info",
            MessageType.Warning => "alert-warning",
            MessageType.Success => "alert-success",
            _ => "alert-danger",
        };

        base.OnPreRender(context);
    }
}

And control is used like this, where MessageType is binded from viewmodel:
<cc:MessageAlert MessageType=ā€œ{value: PageErrorMessageType}ā€ />

What is the correct way to do this derived property logic in code behind? For example, in XAML in DependencyProperty there was something called coerce-value callbacks for this. Is here something similar?
Thanks.

Actually, there is also bug in the properties usage validation. If I used to a non-existing property in the .dotcode, I correctly get this error:
Could not initialize binding ā€˜{value: _control.MessageTypeClass}ā€™, requirements DotVVM.Framework.Binding.Properties.KnockoutExpressionBindingProperty, DotVVM.Framework.Binding.Properties.ResultTypeBindingProperty, DotVVM.Framework.Binding.Expressions.BindingDelegate were not met.

but if the property exists there, but it is implemented as property only (not the DotvvmProperty), this error will not appear, but when rendering on the client, this JS error will show in console:

Uncaught TypeError: Unable to process binding ā€œdotvvm-with-control-properties: function(){return { MessageType:PageErrorMessageType} }ā€
Message: Unable to process binding ā€œtext: function(){return ($control.MessageType() ??ā€ā€œ) +ā€\n"+($control.MessageTypeClass() ??ā€œā€) +ā€œ\n\nā€ }"
Message: $control.MessageTypeClass is not a function

Hi!

The first issue: Yes, @viewModel object is the correct solution. I understand it feel more like a glitch, but ā€œno view modelā€ does not make sense in dotvvm, what you actually want is to allow any view model.

Second issue:

You are right, Iā€™d also say itā€™s a bug that we donā€™t report error when a plain .NET property is used. Using PreRender a correct workaround, alternatively you could use an instance method on the control, or static method defined anywhere else. Both of these options are however not reexecuted on a postback - the value get hardcoded into the HTML and DotVVM only updates the view model.

You have two options if you want to update it on postback

  • Use the PostBack.Update property, that will make DotVVM regenerate the HTML on each postback. See Server-side rendering | DotVVM Documentation
  • Write it in a way DotVVM can translate to the Javascript expression. This may actually be very easy in your case - If you apply [EnumMember(Value = "desired-js-name")] attribute the enum fields, dotvvm will represent the enum client-side using these names, so can replace your function by letting it implicitly convert to string, or using the ToEnumString function

Hi, thanks for that.

How to use PostBack.Update=ā€œtrueā€?
If I use it on control usage, its working:
<cc:MessageAlert MessageType=ā€œ{value: PageErrorMessageType}ā€ PostBack.Update=ā€œtrueā€ />

But how do I achieve this behavior inside custom control definition, in MessageAlert.dotcontrol or control code behind?

As for the JS solution, this is how it works, but itā€™s little ugly.

{{value: _control.MessageType == 'Info' ? 'alert-info' : _control.MessageType == 'Warning' ? 'alert-warning' : _control.MessageType == 'Success' ? 'alert-success' : 'alert-danger'}}

It would be nice to support something like this for this kind of cases.
{{value: ({ Info: 'alert-info', Warning: 'alert-warning', Success: 'alert-success', Error: 'alert-danger'})[_control.MessageType])}}
which is a JS variant something like a switch expression.

But I still need the PostBack.Update solution though, because there is another property that canā€™t be done with an js expression translation.

Thanks

It should work even if you place it inside the control, you can put it on just the <span> with your label. It shouldnā€™t be placed inside client-side rendered Repeaters and similar controls. You can also set it in code-behind using control.SetValue(PostBack.Update, true) (but you have to be careful about control unique idsā€¦)

If this doesnā€™t work, itā€™s of course possible itā€™s buggy in some case. Does it fail with some error? (it would probably be an client-side error, most likely the control ids donā€™t match)

Well, you can always use knockout data bindings directly (data-bind='text: ({ Info: 'alert-info', Warning: 'alert-warning', Success: 'alert-success', Error: 'alert-danger'})[$control.MessageType()]'). Note that itā€™s necessary to use the _control.MessageType in some value binding, otherwise DotVVM wonā€™t bother to send it to the client (Iā€™d recomment <dot:Placeholder IncludeInPage=false>{{value: _control.MessageType}}</dot:Placeholder>)

This working.

protected override void OnInit(IDotvvmRequestContext context)
{
    SetValue(PostBack.UpdateProperty, true);

    base.OnInit(context);
}

Thanks, thatā€™s what I was looking for.

Great!

Iā€™d still recommend placing the smallest possible subtree in Postback.Update. Also prefer to use resource bindings to force server-side only rendering. I think that this might not have worked for you, because the control property is serialized in the control header and the alert element only references that value. If you use a resource binding instead of value binding to bind the property, the value gets hardcoded into element and no references interfere with that.