Defer'd javascript

Jun 8, 2011 at 5:09 AM

Hi,

I've got a case where I have a partial page I am loading based on an AJAX request.  This partial page contains controls from the MVC toolkit.  Is there a way to enable these control's scripts to work?

One thing I've done is added a "defer" to all javascript elements prior to putting on the page (via an innerHTML element) but the DateTime functions are not working at all which leads me to believe there is also some additional scripts that need to fire.  Can you help?

Thanks,

Craig

Coordinator
Jun 8, 2011 at 8:47 AM

Hi Craig,

All Mvc Controls Toolkit controls do work also when used in ajax updates....not only you can also use them in knockout client side templates. You donb't need to do ANYTHING, no deferred execution is needed. Just append the html received from your ajax to your target container...stop. Any other action may change the right timing of javascript execution. 

Anyway, I advice to use jQuery functions to append your html. Better to use Mvc Ajax links or Ajax BeginForm. innerHtml may have a behaviour that depend of the browser.

 

HOWEVER PAY ATTENTION.  Don't play around with the html received by ajax because either if you use innerHtml, or jQuery html, append, or similar, all javascript is executed immediately after the new content is appended to its target, and then IT IS IMMEDIATELY DELETED FROM THE DOM, to avoid it is executed again if the DOM node it is contained is moved to a new position...The only chance to execute some javascript is just at the moment the new content is appended to the dom....then there will be no javascript to execute anymore.

If you continue experiencing problem, please post some code, so I can help you.

Jun 8, 2011 at 8:55 AM

OK, so the innerHtml is a no go.  So if I get a Partial page back from the client that I need to put in the page how should I do it?

e.g. I have var myHTML = .....someHTML from the server....

I am currently doing

$('#MyCont')[0].innerHTML = myHTML

Where MyCont is a <div> But that doesn't fire the Javascript unless I put a defer on it.

What should I do instead?

Thanks,

Craig

Jun 8, 2011 at 8:17 PM

OK, I'm still having troubles with this.  I think my current MVC3 knowledge is at a limit and I'm trying to make it do things like I used to do them in ASPX.

Here is some more details:

I have a DropDown list on a page where when it changes I want to load a Partial Page depending on the selection.  This is due to the selection causing a vast amount of selection parameters that need to be entered.

So I have wired up an event to the dropdown list as follows:

$(document).ready(function () {
    $('#TransactionType').change(function () {
        $.get('@Url.Action("GetPartial", "Transaction")',
            { transType: $(this).val() },
            function (data) {
                $('#TransComp').empty();
                $('#TransComp')[0].innerHTML = data;
            });
    });
});

My server side controller code for the GetPartial is this (currently incomplete)

public ActionResult GetPartial(string transType)
{
    switch (transType)
    {
        case "PC":
        case "SA":
            return PartialView("_EquityTransaction");
        default:
            break;
    }
    return null;

Now my Partial view is just a basic set of controls and I have some JS on that page as well.  So the question really is how can I get that partial page to Render with the JS stuff wired up as well.

I'm looking at the Ajax.BeginForm now and I am guessing I need to set the post back on that drop down somehow.

Cheers,

Craig

Coordinator
Jun 8, 2011 at 8:48 PM

Hi Craig,

Please do not panic you don't need to acquire too much knowledge. I suggest the following, as first step let continue using the $.get jquery function that is ok. You can also study the Mvc 3 ajax functions AFTER you have this woirking. Consider that the Mvc 3 ajax fucntions in turn use the jquery ajax functions, and you cannot apply them ALWAYS. Sometime is sipler to use $.get or similar. In you case you might apply an ajax form and the causing a sumbim on the dropdown change event.....However I advice continuing with $.get that should work, since you know it better.

 

Now let go to the code. I suggest you use  just...$('#TransComp').html(data); that'ts all! This should apply all html in a browser independent way, and the execute the javascript contained in the html. The remainder of the javascript appears to be ok. Then, when you have this working you can add also client side validation by parsing the html you just attached with the unobtrusivae validation parsing functions...wait before doing this because there is a bug in the parsing function so maybe you have to use a patched parsing function (it is included in the toolkit).

 

Try this, if javascript doesn't work, please put a breakpoint and see the content of data (the html you are trying to add). Copy it and show me it, so I can try to understand possible problems.

Jun 8, 2011 at 9:53 PM

I have progressed and am now using AJAX to get back the partial form as below:

@using (Ajax.BeginForm("GetPartial", new AjaxOptions { UpdateTargetId = "TransComp", OnSuccess="PostActions()" }))
{
    <p>
    @Html.LabelFor(model => model.TransactionType)
    @Html.DropDownListFor(model => model.TransactionType, Model.AllTransactionTypes, new { onchange = "$('#ttc').click()" })
    @Html.ValidationMessageFor(model => model.TransactionType)
    </p>
    <input type="submit" id="ttc" style="display:none" />
}

This runs all the JS fine but the datetime fields in the partial view don't work still.  It appears there needs to be some initialisation done.

Any ideas?

Craig

Jun 8, 2011 at 10:45 PM

Just as a note, the first time it loads it renders the Calendar input box without a class="hasDatepicker" on it.  If I refresh the page (with the partial on it) it places this class on the input and everything works fine.

I now need to find where that "hasDatepicker" class is set.

Craig

Jun 8, 2011 at 10:59 PM

and this appears to be how I can do it:

$(':input[id$="_Calendar"]').datepicker();

 

 

After the partial class has been loaded.

Coordinator
Jun 9, 2011 at 10:26 AM

Hi Craig,

That is not my code but jQuery plugin that doesn't initialize properly. We have to understand why. There are just two possibilities:

1) The first time you render the partial view you use a quite different code than in the other refresh, and in this code something wrong is done.

2) There is a complex bug due to the timing on the vaious operations done by my server controls and jQuery plugin

 

I tried a similar example on my computer and it works properly. However this does'nt means that there is no bug. The problem may arise because of sime tyme critical operation you do and that I have not in my code.

Please post me the code of the partial views that contain the datatimeinput. If you use two different partial view, the first for the first time and the other quen you submit the partial view, please shome both.

If I am not able to find the problem this way you can send me a self contained project that shows the problem (possibly no database) and I will debug it.

Anyway, the time you are spending this way is not wasted because you are acquirin "actual practical knowledge"....better thatn the one you can acquire reading. This is also the way I preferr learnig things...putting my fingers in the source code :)

Jun 9, 2011 at 8:02 PM

Interesting...I believe the bug is purely caused because when the partial view is loaded (via ajax) the initialisation of the datepickers is already completed for the page.  By redoing the partial view fetch the components for the datetime are already in the DOM so when the page fires it is already loaded for the second time.  It appears that an AJAX component does not get the scripts for the page to run on it when it is placed on a view.

My code is very simple.  My partial view contains this (among a few other things):

<p>
 @Html.LabelFor(model => model.TransactionDate)
 @Html.DateTimeFor(model => model.TransactionDate, DateTime.Now, dateInCalendar: true).DateCalendar(null, false, null)
 @Html.ValidationMessageFor(model => model.TransactionDate)
</p>

The Main view contains this (to postback for the partial):

<fieldset class="details">
 <legend>Transaction Details</legend>
    @using (Ajax.BeginForm("GetPartial", new AjaxOptions { UpdateTargetId = "TransComp", OnSuccess="PostActions()" }))
    {
        <p>
        @Html.LabelFor(model => model.TransactionType)
        @Html.DropDownListFor(model => model.TransactionType, Model.AllTransactionTypes, new { onchange = "$('#ttc').click()" })
        @Html.ValidationMessageFor(model => model.TransactionType)
        </p>
        <input type="submit" id="ttc" style="display:none" />
    }
</fieldset>
<fieldset>
 <div id="TransComp"></div>
</fieldset>

and this for the post actions:

<script type="text/javascript">
    function PostActions() {
        LoadKeyCaptcher();
        ManualLoadAutoComplete();
        SetDatePickers();
    }
</script>

which simply redo the assigning etc on the elements of the ajax page.

The Controller then for the Main view which returns the partial is this (where _EquityTransation is my partial view):

public PartialViewResult GetPartial(string TransactionType)
{
    switch (TransactionType)
    {
        case "PC":
        case "SA":
            Equity eq = new Equity();
            eq.TransactionType = TransactionType;
            eq.PortfolioAllocations = new List<Tracker<EquityPortfolio>>();
            return PartialView("_EquityTransaction", eq);
        default:
            break;
    }
    return null;
}

That is pretty much all I have been doing.  You will see that it wasn't just the date pickers I had to redo.  There were some other items as well like auto complete and key captchers that weren't wired up for the ajax partial view.  I have also noticed that the MVC3 Validations of the fields are also not working so I will need to investigate how to manually redo those as well....any ideas?

Cheers,

Craig

Coordinator
Jun 9, 2011 at 10:58 PM
Edited Jun 9, 2011 at 11:06 PM

It is normal that client validation doesn't fire for elements that were attached through ajax. You have to call the MVC unobtrusibe validation parsing function to get client validation works. However as I said there is a deep bug in the jQuery plugin validation system that prevent to add new validation rules to an exixsting form. So if the ajax content is returned into an empty form or completely substitute the content of a form the parsing function then the parse function works, otherwise you have to call a patched version of the parsing function. Tomorrow I will write more details...now it is very late in Italy. 

 

I will study your code and tomorrow I will return some news. When the vie is rendered the first time is the <div id="TransComp"></div> empty? If it is empty this means it get populated at the first dropdown change, right? if it is not empty, how ist it populated??? The initial rendering of this div might be the cause of the problem....because if you populate it through ajax the ajax content might rieah the  page before the page has finished its initialization....please let me understand well this point!!!!!

By theway why you call the SetDatePickers(); on the OnSuccess of the ajax call? Why don't you set the right value of the pickers when you render the picker on the server? When you do new Equity() you can assign all initial values to all its properties included TransactionDate...so why you set the values for the datepicker with the SetDatePicker function?

 

One more thing on the form you don't need to simulate the click of the submit button, but in place of $('#ttc').click() you can write: $('#MyForm').submit() where MyForm is the id of the form(you can give andn id to the form by exploiting its htmlAttributes.

Jun 10, 2011 at 1:26 AM
Edited Jun 10, 2011 at 5:26 AM

I will look at the validation logic.  Thanks.

When the view is rendered the first time the <div id="TransComp"></div> is empty.  It gets populated from the ajax call result of the form.

The SetDatePickers just performs this:

$(':input[id$="_Calendar"]').datepicker();

so it does not set any values on the form, just initialises the date picker so when you click on it it pops up with a calendar, otherwise this didn't work.  However now when I choose a date from this popup calendar it is not setting all the other hidden fields that come from the MVC DateTime control.  Can you point me to what I need to run to initialise this control?

Finally I tried using the $('#MyForm').submit() but this tended to post the whole page back and direct it to the partial view instead of doing the AJAX call.  It took me AGES to figure out what was going on, in the end I tried adding an <input type="submit"> button and when I clicked that it worked perfectly.  I therefore just made this hidden and clicked it via the drop down - it all works perfectly now. 



Craig

 

Coordinator
Jun 10, 2011 at 9:12 AM

Hi Craig,

if you are using unobstructive ajax $('#MyForm').submit() should work properly. On the other hand if you are using ol Microsof Ajax you may experience problems with the form submit. Verify if in your web.config you have:

 <appSettings>
    <add key="ClientValidationEnabled" value="true"/> 
    <add key="UnobtrusiveJavaScriptEnabled" value="true"/> 
  </appSettings>

Otherwise you are using the old Microsof ajax that mixed with document.ready function of jQuery may cause timing problems. 

 

Anyway, please send me a self contained project that shows all your problems. I will review it,and debug it to understand the source of you problems, It make no sense adding patchs to make the DateInput works properly it MUST work. So if there are bugs in Mvc Controls Toolkit I will fix it, or if there are bugs in your code I will report How to solve them. In any case you will have your code working properly with NO PATCH.

In order to send the code please use the contact form of my blog here: http://www.dotnet-programming.com/contact.aspx

You can attach a file...if it is not huge...it should work :)

It will be good for me if you send it this week so I can work on it in the week end. ..I need to release a new bug fix version of the Mvc Controls Toolkit asap since I have some of my customers waiting for this version and if the problems you are experiencing are due to a bug of Mvc Controls toolkit a would like to include a fix for them in this version.

 

About client Validation. Th function to call to enable validation on the new supplied html code is: 

$.validator.unobtrusive.parse(selector)

where selector is a jQuery selector for all the new added html. in your case it should be '#TransComp'.

However if the new html is added to an existing form that already contains other inputelements this function, that is the official Microsoft parsing fucntion....doen't work because of a deep bug difficult to remove(it is due to a mismatch between Microsoft requirements and the way some caching functions of the jQuery plugin work....so fixing it requires a coordination between Microsoft and the validation team of jQuery....), so in my Mvc Controls Toolkit I added a patched version that is:

$.validator.unobtrusive.parseExt(selector)

This fucntion WORKS properly

Jun 11, 2011 at 6:54 AM

I have sent through an example project.  Once you run it and follow the steps you should see the issues I have been having.

Thanks again,

Craig

Coordinator
Jun 11, 2011 at 4:05 PM

Hi Craig,

I fixed your code. You has so many difficulties because you started with somnething very difficult to do. When I say "difficult" I mean not difficult from a programming point of View, but "difficult" for the MVC knowledge required. However, the good thing is that you learned in a short time a deep knowledge about mvc :)

 

Below the correction to be done with some comment. I think it is better tro try to apply youself the correction, so you can learn better all concepts that are behind. If you have problems...I will send you the whole project corrected.

1) When you use jquery, jquery ajax and similar it is better to do ALL THINGs in the jQuery way. Please don't use directly the events of the html elements such as onchange bu apply them in the jQuery way, that is something like:

$(document).ready(function () {
            $('#Type').change(function () {
                ...................
		...................
            });
        });

If you don't remember the sintax pls ho here: http://docs.jquery.com/Main_Page and look for what you need (pls don't be lazy...otherwise at the end you will waste more time). Attachin the EVNTS DIRECTLY CHANGE THE TIMING OF THEIR TRIGGERING, and may cause them trigger when the page has not finished rendering. Moreover, if you do this, you will not be able to attach more than one handler for the same event...and this might cause problems because some other library (for instance my mvc controls toolkit) might add an handler to do its job...and in this case you might have troubles.

2) The best way to submit a form (ajax or not) when you are in an event hendler for an html element is: $(this).parents('form').submit(). This way you find all ancestor of the element the event handler belong to that are forms (there needs to be just one) and submit them...easy :)

Now the code of your main View becomes:

<h2>Calendar Test with AJAX.  Follow the steps through to replicate the issue</h2>

@using (Ajax.BeginForm("GetPartial", new AjaxOptions{UpdateTargetId="SubComp"}))
{
<div id="Selection">
    <p>1: choose an item from this drop down to get the partial page.  This will load the SubComp div using AJAX</p>
    @Html.LabelFor(m => m.Type)
    @Html.DropDownListFor(m => m.Type, Model.AllTypes)
    <script type="text/javascript">
        $(document).ready(function () {
            $('#Type').change(function () {
                $(this).parents('form').submit();
            });
        });
    </script>

</div>
}
<br />
@using (Html.BeginForm())
{
    <div id="SubComp"></div>
    <p>6: Now you can click the submit button to send to the server.  This should get the new date selected from the calendar</p>
    <input type="submit" name="Submit Details" />
}
<br />
<br />

HOWEVR ALL THIS JUST CLEAN UP YOUR CODE and get rid of possible problems. 

 

Your core error is the fact that you send to the client input controls that are not enclosed in a form. All MVC input helpers doesn't work properly when they are rendered without being enclosed in a form !!! The fact that you add them in a form once they reach the client doesn't help.

The reason of the problem is taht input helpers needs to be rendered in a ViewContext, and a form automatically create a ViewContext. They use this ViewContext as a kind of blackboard to write and read some infos useful for their rendering.

Now you can either create manually a ViewContex in your controller or partial View, or use a special Action Result that is in the Mvc Controls Toolkit that do this job for you the ClientValidationViewResult, see here: http://mvccontrolstoolkit.codeplex.com/wikipage?title=Action%20Filters

With this modification your action method becomes:

public ActionResult GetPartial(string Type)
        {
            return new MVCControlsToolkit.Controller.ClentValidationViewResult("_SubComponent", null);
        }

The null parameter is the ViewModel, that in yoiur case is null.

Jun 12, 2011 at 1:03 AM

Wow, you're a legend.  Thanks for all the input and it all makes perfect sense.  I've only been using MVC for a month so am a bit new with it all...good getting feedback on it all, and the jQuery peice as well.

It all works perfectly now.

Craig

Jun 12, 2011 at 8:25 PM
Edited Jun 13, 2011 at 1:29 AM

I just included these changes in to my main project and have noticed something that isn't right still.  When the partial page is rendered it picks up everything from the main _Layout view.  This includes all information within that View like headings, logo's, menu's etc.  My partial doesn't include any of that on it so it must have obtained it from the _Layout.  Is there anyway to switch this off so it only renders my partial pages content?

To duplicate this issue add an <h1> tag in the _Layout page and when it renders the partial the <h1> is re-rendered on that page as well.

Thanks,

Craig

Coordinator
Jun 13, 2011 at 8:35 AM

try add this to your view:

@{
    Layout = "";    
    }

Jun 13, 2011 at 8:08 PM

Thanks,

Was just about right.  This though didn't apply all the scripts from the _Layout.  What I did was created a new Layout called _PartialLayout which contained just the raw HTML required with no headings/menus etc.  I then set the Layout to this and it rendered fine.

Thanks for putting me on the right track.

Cheers,

Craig