[CLOSED] Custom control with direct methods and remote validation

Page 1 of 2 12 LastLast
  1. #1

    [CLOSED] Custom control with direct methods and remote validation

    Hello support team,
    I want to create a custom control with server methods placed inside the control and called from an associated JavaScript class. I tried to use DirectMethod and RemoteValidation for my purpose, but I'm not able to find a way how to call these server methods (both App.direct and Ext.net.DirectMethods are not defined in Ext.ux.form.ValidatedField).

    using Ext.Net;
    using System;
    using System.Collections.Generic;
    using System.Linq.Expressions;
    using System.Web.UI;
    
    [assembly: WebResource("Nelis.Components.TextFields.ValidatedField.js", "text/javascript")]
    
    namespace Nelis.Components
    {
        public partial class ValidatedField : TextField
        {
            public ValidatedField() {
                IsRemoteValidation = true;
                RemoteValidation.Type = DirectEventType.Load;
                RemoteValidation.ValidationEvent = "blur";
            }
    
            protected override void OnInit(EventArgs e)
            {
                base.OnInit(e);
    
                if (Vtype == "vtype1") RemoteValidation.Validation += new RemoteValidationDirectEvent.RemoteValidationEventHandler(MyValidateMethod1);
                if (Vtype == "vtype2") RemoteValidation.Validation += new RemoteValidationDirectEvent.RemoteValidationEventHandler(MyValidateMethod2);
            }
    
            private void MyValidateMethod1(object sender, RemoteValidationEventArgs e)
            {
                throw new NotImplementedException();
            }
    
            private void MyValidateMethod2(object sender, RemoteValidationEventArgs e)
            {
                throw new NotImplementedException();
            }
    
            [DirectMethod]
            public void MyDirectMethod(string vtype, string val)
            {
                Ext.Net.X.Msg.Alert("DirectMethod", "Direct method reached: " + vtype + " / " + val).Show();
            }
    
            public override string XType
            {
                get { return "validatedfield"; }
            }
    
            public override string InstanceOf
            {
                get { return "Ext.ux.form.ValidatedField"; }
            }
    
            protected override List<ResourceItem> Resources
            {
                get
                {
                    List<ResourceItem> baseList = base.Resources;
                    baseList.Capacity += 1;
                    baseList.Add(
                        new ClientScriptItem(typeof(ValidatedField),
                            "Nelis.Components.TextFields.ValidatedField.js",
                            "Nelis.Components.TextFields.ValidatedField.js",
                            "",
                            ""
                            ));
                    return baseList;
                }
            }
        }
    
        #region EXT.NET
        public partial class ValidatedField
        {
            new public class Builder<TField, TBuilder> : TextField.Builder<TField, TBuilder>
                where TField : ValidatedField
                where TBuilder : Builder<TField, TBuilder>
            {
                public Builder(TField component) : base(component) { }
            }
    
            new public class Builder : Builder<ValidatedField, Builder>
            {
                public Builder() : base(new ValidatedField()) { }
                public Builder(ValidatedField component) : base(component) { }
            }
        }
    
        public partial class NControlFactory
        {
            public ValidatedField.Builder ValidatedField()
            {
                return ValidatedField(new ValidatedField());
            }
    
            public ValidatedField.Builder ValidatedField(ValidatedField component)
            {
                component.ViewContext = this.HtmlHelper != null ? this.HtmlHelper.ViewContext : null;
                return new ValidatedField.Builder(component);
            }
        }
    
        public partial class NControlFactory<TModel>
        {
            public ValidatedField.Builder ValidatedFieldFor<TProperty>(Expression<Func<TModel, TProperty>> expression, bool setId = true, Func<object, object> convert = null, string format = null)
            {
                return InitFieldForBuilder<ValidatedField, ValidatedField.Builder, TProperty>(expression, setId, convert, format);
            }
        }
        #endregion
    }
    Ext.ns("Ext.ux.form");
    
    Ext.define("Ext.ux.form.ValidatedField", {
    
        extend: "Ext.form.field.Text",
        alias: "widget.validatedfield",
    
        initComponent: function () {
            this.callParent();
    
            this.validateOnChange = false;
    
            var me = this;
    
            Ext.apply(Ext.form.VTypes, {
                vtype1: function (val, field) { return me.vtype1(val, field); },
                vtype1Text: "invalid vtype1",
                vtype2: function (val, field) { return me.vtype2(val, field); },
                vtype2Text: "invalid vtype2"
            });
    
        },
    
        vtype1: function (val, field) {
            if (!val) return;
    
            console.log("VAL", val);
    
            // => return MyDirectMethod(field.vtype, val) result
    
            return true;
        },
    
        vtype2: function (val, field) {
            if (!val) return;
    
            console.log("VAL", val);
    
            // => return MyDirectMethod(field.vtype, val) result
    
            return true;
        }
    
    });
    According to the thread Difference between DirectMethod, DirectEvent, Static DirectMethod, it should be possible to use a direct method defined inside user control or custom control, but I can't figure out how it should work.

    Of course, I can use Ajax calls and place the required methods somewhere in the controller, but that's not what I would prefer in terms of code compactness.

    I also wonder if it is possible to ensure that the vtype1 and vtype2 validators are called only for a blur event. Setting this.validateOnChange = false causes that the validity is not evaluated immediately as the user is typing, but the function is still called after each change of the input value, which is not desirable for a potential server method.

    Can you please confirm that my intention is feasible and give me hints on how to achieve this?

    Thank you for your assistance.

    Ext JS 7.3.1.27 / Ext.NET 5.3.0

    Kind regards
    Dan
    Last edited by fabricio.murta; Feb 19, 2021 at 1:55 PM.
  2. #2
    Hello Dan!

    I think you forgot to assign the direct method to ResourceManager. When you do from a component, you need this extra step.

    I believe this is the post you were looking for: Post #4 in Feature request - support for Static Direct Methods in custom Components.

    Quote Originally Posted by NewLink
    I also wonder if it is possible to ensure that the vtype1 and vtype2 validators are called only for a blur event. Setting this.validateOnChange = false causes that the validity is not evaluated immediately as the user is typing, but the function is still called after each change of the input value, which is not desirable for a potential server method.
    That's an old old bug in the Ext JS framework... It seems by adding this to the js' initComponent function improves it:

    this.valuePublishEvent = 'blur';
    But I still get the direct method called 5x each blur, will have to investigate that as well.

    You will also need to pay mind to the return of the direct method. It only returns something similar to a promise, where you should bind callbacks to handle the actual return from the direct method (if it returns true/false for the validity status).

    So probably the vtype functions calling direct methods should always return its current validity and let the success callback switch the field's validity with the actual result.

    Outline:

    App.direct[this.id].MyDirectMethod(field.vtype, val, {
        success: function (result) { Ext.toast("returned: " + JSON.stringify(result)); }
    });
    Hope this helps!
    Fabrício Murta
    Developer & Support Expert
  3. #3
    Hi Fabrício,
    thank you for an answer.

    Unfortunately, registering DirectMethod using ResourceManager.AddDirectMethodControl does not help, the proxy is not generated.

    Also, using this.valuePublishEvent = "blur" does not help and the validator function still fires while typing text.

    One possibility seems to be to maintain some kind of blur event flag, which is then tested in a validator:

    this.on("focus", function (field, event) { this._validateMe = false; }, this);
    
    this.on("blur", function (field, event) { this._validateMe = true; }, this);
    
    vtype1: function (val, field) {
            if (!val || !field._validateMe) return;
    
            return val.length > 3;
        },
    One curiosity: when this.on("blur", this.onBlur, this) was used, then RemoteValidation defined for component stopped to work.

    Kind regards
    Dan
  4. #4
    Quote Originally Posted by NewLink
    Unfortunately, registering DirectMethod using ResourceManager.AddDirectMethodControl does not help, the proxy is not generated.

    Also, using this.valuePublishEvent = "blur" does not help and the validator function still fires while typing text.
    Sorry, Dan, I think we are not on the same page. Can you provide a full test case? The thread I provided has a full simplified test case on the AddDirectMethodControl in case you need something to start with.

    Albeit there being an issue with the blur event, the suggested code works on my side if I added the line to the implementation of the custom component. It only triggers the direct method once I navigate out of the field.

    Besides, in the code you provided, you are assigning both the direct method and direct event at the same time, I suggest you disable either one while testing because this may get confusing even for you. We probably should deal with either the direct event or direct method approaches separately. Having them both wired up to the component would just make multiple server calls for (apparently) the same end.
    Fabrício Murta
    Developer & Support Expert
  5. #5
    Hello Fabrício,
    thank you for answer. I apologize if my test case wasn't clear enough. Here is all the source code exactly as it was tested.

    NControlFactory
    using System.Web.Mvc;
    
    namespace TestCases.Components
    {
        public partial class NControlFactory
        {
            public HtmlHelper HtmlHelper { get; set; }
        }
    
        public partial class NControlFactory<TModel> : NControlFactory
        {
            HtmlHelper<TModel> htmlHelper;
            new public HtmlHelper<TModel> HtmlHelper
            {
                get
                {
                    return this.htmlHelper;
                }
                set
                {
                    this.htmlHelper = value;
                    base.HtmlHelper = value;
                }
            }
        }
    
        static class NFactory
        {
            private static NControlFactory _builder = null;
    
            public static NControlFactory Builder
            {
                get
                {
                    if (_builder == null) _builder = new NControlFactory();
                    return _builder;
                }
            }
    
            internal static NControlFactory BuilderHelper(HtmlHelper htmlHelper)
            {
                return new NControlFactory { HtmlHelper = htmlHelper };
            }
    
            internal static NControlFactory<TModel> BuilderHelper<TModel>(HtmlHelper<TModel> htmlHelper)
            {
                return new NControlFactory<TModel> { HtmlHelper = htmlHelper };
            }
        }
    
        public static class NExtensions
        {
            public static NControlFactory N(this HtmlHelper htmlHelper)
            {
                NFactory.Builder.HtmlHelper = htmlHelper;
                return NFactory.BuilderHelper(htmlHelper);
            }
    
            public static NControlFactory<TModel> N<TModel>(this HtmlHelper<TModel> htmlHelper)
            {
                NFactory.Builder.HtmlHelper = htmlHelper;
                return NFactory.BuilderHelper<TModel>(htmlHelper);
            }
        }
    }
    Component C#
    using Ext.Net;
    using System;
    using System.Collections.Generic;
    using System.Web.UI;
    
    [assembly: WebResource("TestCases.Components.ValidatedField.ValidatedField.js", "text/javascript")]
    
    namespace TestCases.Components
    {
        public partial class ValidatedField : TextField
        {
            public ValidatedField() { }
    
            protected override void OnInit(EventArgs e)
            {
                base.OnInit(e);
                // 1. I tried to configure RemoteValidation to call a method placed directly in the component itself.
                //    Unable to figure out how to do it, I abandoned this concept and replaced it with the traditional controller call:
    
                //if (Vtype == "vtype2") RemoteValidation.Validation += new RemoteValidationDirectEvent.RemoteValidationEventHandler(ValidateVType2);
    
                if (Vtype == "vtype2")
                {
                    IsRemoteValidation = true;
                    RemoteValidation.Url = "/ValidatedField/CheckField/";
                    RemoteValidation.Type = DirectEventType.Load;
                    RemoteValidation.ValidationEvent = "blur";
                    RemoteValidation.ShowBusy = false;
                }
            }
    
            private void ValidateVType2(object sender, RemoteValidationEventArgs e)
            {
                throw new NotImplementedException();
            }
    
    
            protected override void OnLoad(EventArgs e)
            {
                base.OnLoad(e);
    
                // 2. Then I tried to call DirectMethod placed directly into the component itself.
                //    Again, no luck because the App.direct proxy is not created: 
    
                this.ResourceManager.AddDirectMethodControl(this);
    
                //if (Vtype == "vtype3") Listeners.Blur.Handler = "App.direct[this.id].MyDirectMethod();";
            }
    
            [DirectMethod]
            public bool MyDirectMethod()
            {
                Ext.Net.X.Msg.Alert("DirectMethod", "Direct method reached.").Show();
    
                return false;
            }
    
            public override string XType
            {
                get { return "validatedfield"; }
            }
    
            public override string InstanceOf
            {
                get { return "Ext.ux.form.ValidatedField"; }
            }
    
            protected override List<ResourceItem> Resources
            {
                get
                {
                    List<ResourceItem> baseList = base.Resources;
                    baseList.Capacity += 1;
                    baseList.Add(
                        new ClientScriptItem(typeof(ValidatedField),
                            "TestCases.Components.ValidatedField.ValidatedField.js",
                            "TestCases.Components.ValidatedField.ValidatedField.js",
                            "",
                            ""
                            ));
                    return baseList;
                }
            }
        }
    
        #region EXT.NET
        public partial class ValidatedField
        {
            new public class Builder<TField, TBuilder> : TextField.Builder<TField, TBuilder>
                where TField : ValidatedField
                where TBuilder : Builder<TField, TBuilder>
            {
                public Builder(TField component) : base(component) { }
            }
    
            new public class Builder : Builder<ValidatedField, Builder>
            {
                public Builder() : base(new ValidatedField()) { }
                public Builder(ValidatedField component) : base(component) { }
            }
        }
    
        public partial class NControlFactory
        {
            public ValidatedField.Builder ValidatedField()
            {
                return ValidatedField(new ValidatedField());
            }
    
            public ValidatedField.Builder ValidatedField(ValidatedField component)
            {
                component.ViewContext = HtmlHelper != null ? HtmlHelper.ViewContext : null;
                return new ValidatedField.Builder(component);
            }
        }
        #endregion
    }
    Component JS
    Ext.ns("Ext.ux.form");
    
    Ext.define("Ext.ux.form.ValidatedField", {
    
        extend: "Ext.form.field.Text",
        alias: "widget.validatedfield",
    
        _validateMe: false,
    
        initComponent: function () {
            this.callParent();
    
            // 3. The vtype1 and vtype2 validators are still fired when typing text, not just once on blur events.
            //    To force executing validation only after leaving the field, I introduced the _validateMe flag: 
    
            this.on("focus", function () { this._validateMe = false; }, this);
            this.on("blur", function () { this._validateMe = true; }, this);
    
            // 4. When used this.on("blur", this.onBlur, this) and onBlur function was defined here, RemoteValidation stopped to work.
    
            this.validateOnChange = false;
            this.valuePublishEvent = "blur";
    
            var me = this;
    
            Ext.apply(Ext.form.VTypes, {
                vtype1: function (value, field) { return me.vtype1(value, field); },
                vtype1Text: "invalid vtype1",
                vtype2: function () { return true; }, // => REMOTE validation
                vtype3: function (value, field) { return me.vtype3(value, field); },
                vtype3Text: "invalid vtype3",  // => DIRECT METHOD validation
            });
        },
    
        vtype1: function (value, field) {
            if (!value || !field._validateMe) return;
    
            return value.length > 3;
        },
    
        vtype3: function (value, field) {
            if (!value || !field._validateMe) return;
    
            // App.direct is not created:
            //App.direct[this.id].MyDirectMethod(field.vtype, val, {
            //    success: function (result) { Ext.toast("returned: " + JSON.stringify(result)); }
            //});
    
            return true;
        }
    });
    Controller
    using System.Web.Mvc;
    
    namespace TestCases.Controllers
    {
        public class ValidatedFieldController : Controller
        {
            public ActionResult Index()
            {
                return View();
            }
    
            public JsonResult CheckField(string value)
            {
                return new JsonResult
                {
                    Data = new
                    {
                        valid = value == "qwe",
                        message = "Invalid remote validation",
                        value
                    }
                };
            }
        }
    }
    View
    @using Ext.Net;
    @using Ext.Net.MVC;
    @using TestCases.Components;
    
    @{
        Layout = null;
    
        var X = Html.X();
        var N = Html.N();
    
        var f = new ValidatedField();
    }
    
    <!DOCTYPE html>
    
    <html>
    <head>
        <title>Ext.NET MVC Test Case</title>
    </head>
    
    <body>
        @X.ResourceManager()
    
        @X.DisplayField().ID("version").ReadOnly(true).Margin(10).Width(200)
    
        @N.ValidatedField().FieldLabel("VType1").Margin(10).AutoFocus(true).Vtype("vtype1")
    
        @N.ValidatedField().FieldLabel("VType2").Margin(10).AllowBlank(false).Vtype("vtype2")
    
        @N.ValidatedField().FieldLabel("VType3").Margin(10).Vtype("vtype3")
    </body>
    </html>
    
    <script type="text/javascript">
        Ext.onReady(function () {
            Ext.getCmp("version").setValue("Ext JS " + Ext.getVersion().version + " / " + "Ext.NET " + Ext.net.Version);
        });
    </script>
    The problems are commented directly in the code, they are summarized here for your convenience:

    1. I tried to configure RemoteValidation to call a method placed directly in the component itself. Unable to figure out how to do it, I abandoned this concept and replaced it with the traditional controller call.

    2. I also tried to call DirectMethod placed directly into the component itself. Again, no luck because the App.direct proxy is not created.

    3. The vtype validators are still fired when typing text, not just once on blur events. To force executing validation only after leaving the field, I introduced the _validateMe flag.

    4. When used this.on("blur", this.onBlur, this) and onBlur function was defined in the component, RemoteValidation stopped to work.

    Kind regards
    Dan
  6. #6
    Hello again, Dan!

    Your test case is pretty comprehensive and we may need an additional time to check it and identify what in it makes the suggestions not help, and what would actually do.

    Besides, this bit posted before:

    Quote Originally Posted by fabricio.murta
    But I still get the direct method called 5x each blur, will have to investigate that as well.
    Still needs investigation. We'll try to get to the same state you are in, and then provide you with appropriate responses.

    We'll post back here soon!
    Fabrício Murta
    Developer & Support Expert
  7. #7
    Hello again, Dan!

    I might not be able to answer all questions now but will at least cover some, and then follow later with answer for the remaining ones.

    The great overall new here is you are in MVC. And this works differently than the (first assumed) WebForms approach where it regards to handling direct methods, and also direct events. I understand it makes sense you would be in MVC due to the factory-related code, but there were no further background on how were you trying that in MVC -- or even if you just set the factory by mistake.

    Quote Originally Posted by NewLink
    1. I tried to configure RemoteValidation to call a method placed directly in the component itself. Unable to figure out how to do it, I abandoned this concept and replaced it with the traditional controller call.
    I'm not sure I understand your statement here. Is this RemoteValidation based in an example we have in EE so I could understand what you're talking about? Nevertheless, It will probably not work if you are in MVC, so I believe the traditional controller call concept you switched to should be the right path to follow.

    Quote Originally Posted by NewLink
    2. I also tried to call DirectMethod placed directly into the component itself. Again, no luck because the App.direct proxy is not created.
    I see, you aren't getting the proxy because the controller does not have the action. The solution I provided works for WebForms. In order to have the direct method available in the MVC concept, you would need not only to define the direct method as an action within the controller, but also (you could just "wrap its call").

    You could define an abstract controller for controllers using the component to inherit from in order to get the Direct method; and this direct method could wrap the call to the original direct method This may be enough to wrap the direct method call, and then you could inherit controllers using the component from this abstract controller:

    public abstract class c63080_ValidatedFieldController : Controller
    {
        [DirectMethod]
        public ActionResult c63080_MyDirectMethod()
        {
            var cmp = new ValidatedField();
            var result = cmp.MyDirectMethod();
    
            return this.Direct(result);
        }
    }
    Then for controllers using the component, just inherit from that class, or:

    [DirectController]
    public class ValidatedFieldController : c63080_ValidatedFieldController
    Notice the [DirectController] attribute decorating the class name. You are missing it in your controller sample code.

    Quote Originally Posted by NewLink
    3. The vtype validators are still fired when typing text, not just once on blur events. To force executing validation only after leaving the field, I introduced the _validateMe flag.
    Please add the thisvaluePublishEvent = "blur" line before this.callParent() in the js initComponent() function. It seems to be safe to but the parent init call last in the initialization code.

    (this does not get rid of the multi-triggers that still needs to be investigated, though).

    How the client-side code would look like after a couple reviews, adding some visual feedback to the client handlers:

    Ext.define("Ext.ux.form.ValidatedField", {
    
        extend: "Ext.form.field.Text",
        alias: "widget.validatedfield",
    
        initComponent: function () {
            this.validateOnChange = false;
            this.valuePublishEvent = "blur";
    
            var me = this;
    
            Ext.apply(Ext.form.VTypes, {
                vtype1: function (value, field) { return me.vtype1(value, field); },
                vtype1Text: "invalid vtype1",
                vtype2: function () { return true; }, // => REMOTE validation
                vtype3: function (value, field) { return me.vtype3(value, field); },
                vtype3Text: "invalid vtype3",  // => DIRECT METHOD validation
            });
    
            this.callParent();
        },
    
        vtype1: function (value, field) {
            if (!value) return;
    
            Ext.toast("Validate 1");
            return value.length > 3;
        },
    
        vtype3: function (value, field) {
            if (!value) return;
    
            Ext.toast("Validate 3");
            return true;
        }
    });
    Quote Originally Posted by NewLink
    4. When used this.on("blur", this.onBlur, this) and onBlur function was defined in the component, RemoteValidation stopped to work.
    This makes sense to me. Not sure how and where you used this, but sounds like you just replaced default onBlur handler with your own, or whatever this.onBlur was at the time you referenced it.
    Fabrício Murta
    Developer & Support Expert
  8. #8
    Hi Fabrício,
    thank you for answer.

    ad 1) My intention was quite simple: to maintain the server-side validation inside the component to keep all the necessary code in one place (if you use the component in another project, you do not have to remember that it is necessary to transfer additional code to make everything work as it should ). However, I can live with the traditional controller call concept.

    ad 2) It works as you suggested. Thanks.

    ad 3) It works as you suggested. Thanks.

    ad 4) Yes, that was also my guess. I only mentioned this because - although I make extensive use of my own event handlers - it was the first time that anything else had stopped working.

    The only remaining problem is that the vtype functions are triggered twice. But as you mentioned, you're already investigating this issue.

    Thank you for your assistance.

    Kind regards
    Dan
  9. #9
    Hello again, Dan!

    Sorry I'm not posting to respond the last item left here, but we are running late with that and I felt we needed to at least keep you posted we are working on it. We still don't have an answer whether that can be easily avoided, or if that's a bug we need to log, but no matter how I look at the problem, there must have a way to avoid the double event hits.

    We'll post back tomorrow!
    Fabrício Murta
    Developer & Support Expert
  10. #10
    Hello again, Dan!

    The issue with double triggering of the validation is exactly because of the fix this.valuePublishEvent = "blur".

    I have noticed that it does not interfere with the remote validation, just for the client-side validation triggers. That said, with initComponent having:

    this.validateOnChange = false;
    this.valuePublishEvent = "blur";
    - The remote validation method is called just once, so if you're only using remote validation this should be good enough for you.
    - The client-side validations vtype1, vtype3 from your test case will be fired twice.

    In this configuration I noticed the validation is being called twice, one for the blur event and other for the focusLeave one. It seems forcing valuePublishEvent as we did breaks a recent implementation from Sencha that introduced the focusLeave validation event concept.

    Then, something that worked for me is, just set this valuePublishEvent as an empty string so that it does not trigger validation on its own (when it "publishes" a field change to application). In fact you can cycle thru a series of settings to choose what's the best way to have the client-side validation event to trigger.

    option 1: on blur via value publish event:
    this.validateOnChange = false;
    this.validateOnFocusLeave = false;
    this.validateOnBlur = false;
    this.valuePublishEvent = "blur";
    option 2: on blur via own validateOn elimination
    this.validateOnChange = false;
    this.validateOnFocusLeave = false;
    //this.validateOnBlur = false;
    this.valuePublishEvent = "";
    By commenting the line out I just said it should be true in case OnFocusLeave was also false.

    And from this point you can switch these settings (the constant here would be this.validateOnChange = false as that's exactly what you always want to avoid), to use the publish event as either "", "blur", or "focusLeave"; or use the validateOn as both false or one of them true.

    Currently, if both validateOn are true -and- publish event is empty, only one validation call will happen as if only validateOnFocusLeave was true.

    Hope this helps! Let us know if there's still something not handled in your inquiries about validation!
Page 1 of 2 12 LastLast

Similar Threads

  1. Replies: 11
    Last Post: Jun 13, 2014, 11:05 AM
  2. Direct Methods and Custom Control
    By Zdenek in forum 1.x Help
    Replies: 0
    Last Post: Apr 19, 2012, 10:18 PM
  3. Replies: 3
    Last Post: Oct 04, 2011, 8:30 AM
  4. Replies: 3
    Last Post: Jul 11, 2011, 9:43 AM
  5. [CLOSED] Direct Methods with return doesn't work in user control
    By sharif in forum 1.x Legacy Premium Help
    Replies: 5
    Last Post: Nov 13, 2010, 12:00 PM

Tags for this Thread

Posting Permissions