Durandal KO binding when data is fetched in activate - durandal

A parameter governs what data is to be displayed. The parameter is retrieved from activationData in the activate method of the view model and used in a call to a Web Api method. Data is returned, and added to the view model like this
define(['durandal/app', 'knockout', 'moment'],
function (app, config, ko, moment) {
var vm = {
app: app
};
vm.activate = function (activationData) {
vm.ChecklistInstanceId = activationData.ChecklistInstanceId;
$.ajax({
url: "api/ChecklistInstance/" + vm.ChecklistInstanceId,
headers: { Authorization: "Session " + app.SessionToken() }
}).done(function (data) {
$.extend(vm, ko.mapping.fromJS(data));
});
};
return vm;
});
Inspecting the viewmodel immediately after it is extended reveals that it is decorated with observables exactly as expected. For example, vm.Caption() exists and returns the string I expect, and vm.Section() is an appropriately populated observable array, and so on down a fairly elaborate object graph.
The problem is the binding phase has already occurred, and at that time the view model lacks all the observables to which I'm trying to bind.
Two possible strategies suggest themselves:
obtain the parameter earlier
re-bind
I don't know how to do either of those things. Can anyone tell me how to re-organise my code to allow binding to parametrically fetched data?

A third possibility occurred to me:
define(['durandal/app', 'knockout', 'moment'],
function (app, config, ko, moment) {
var vm = {
app: app,
Caption: ko.observable(),
Section: ko.observableArray()
};
vm.activate = function (activationData) {
vm.ChecklistInstanceId = activationData.ChecklistInstanceId;
$.ajax({
url: "api/ChecklistInstance/" + vm.ChecklistInstanceId,
headers: { Authorization: "Session " + app.SessionToken() }
}).done(function (data) {
var foo = ko.mapping.fromJS(data);
vm.Caption(foo.Caption());
vm.Section(foo.Section());
});
};
return vm;
});
This works because all the observables exist in the binding phase. This might seem surprising given that I describe only the root of a potentially deep object graph, but the fact that the observable array is empty causes the binding phase to exit without a hitch.
Later in the activate handler, values are added to the observable array after ko.mapping has its way with the data, and binding succeeds.
I have a sense of dèja vu from this: it is eerily reminiscent of problems solved using forward declarations in TurboPascal back in the eighties. La plus ça change...

In order to work on a fully-constructed view, you will need to move your logic to either the attached handler or the compositionComplete handler. As you said, at the activate stage, the DOM isn't yet fully constructed. You can read about these lifecycle callbacks here.
Typically, what we do is pass the activationData in through the activate handler, store the activationData locally (if your viewModel is instance-based, then on a property in the constructor), and then reference that activationData in the attached or the compositionComplete handler.
You can fetch the data in the activate handler, and then store the data locally. But that's all you should do there. Reserve view-related logic for later in the cycle. In this case, you may need to return a promise from activate, and then resolve upon receiving your data. You can read about it here.
UPDATE
Take a look at this post, and the conversation there.

Related

vuex-persistedstate not saving class methods

I'd like to preference this by saying my backgrounds in in C# so I like declaring methods within my classes. I've created a user class that contains properties and methods and I've added this to my vuex-persistedstate. One of the methods is a logout() method which clears out the properties. When I tried to invoke this method I got the following error:
TypeError: this.$data.user.logout is not a function
I then reviewed local storage and noted the user did not have reference to the class method. So I went ahead and copied the logic from the method into my vue component and it worked so I'm assuming the issue is vuex-persistedstate does not save references to methods which is why the method call did not work.
I'd like to declare the logout method in one location rather than spreading it out across vue components, what is the best practice for accomplishing this? Is it possible to do this in the class declaration or do I need a user helper file?
Sure Berco! My code is also up on GitHub so you can review it there too, but basically it seems to me that vuex does not store methods. The first file you should review is my user.js file:
https://github.com/Joseph-Anthony-King/SudokuCollective/blob/master/SudokuCollective.WebApi/client/src/models/user.js
In this file I have a method called shallow clone which takes the info received from the API and assigns it to the user:
shallowClone(data) {
if (data !== undefined) {
this.id = data.id;
this.userName = data.userName;
this.firstName = data.firstName;
this.lastName = data.lastName;
this.nickName = data.nickName;
this.fullName = data.fullName;
this.email = data.email;
this.isActive = data.isActive;
this.isAdmin = data.isAdmin
this.isSuperUser = data.isSuperUser;
this.dateCreated = data.dateCreated;
this.dateUpdated = data.dateUpdated;
this.isLoggedIn = data.isLoggedIn;
}
}
You of course don't need to abstract this away but I've found it makes the code easier to maintain.
Then in the mounted() lifecycle hook I assign the user received from the API to the component user via the shallowClone method. Please bear in mind I've done additional work on this project and the login form is now it's own component which receives the user as a prop from the app:
https://github.com/Joseph-Anthony-King/SudokuCollective/blob/master/SudokuCollective.WebApi/client/src/components/LoginForm.vue
mounted() {
let self = this;
window.addEventListener("keyup", function (event) {
if (event.keyCode === 13) {
self.authenticate();
}
});
this.$data.user = new User();
this.$data.user.shallowClone(this.$props.userForAuthentication);
},
The full code can be reviewed here:
https://github.com/Joseph-Anthony-King/SudokuCollective
I found a solution... I'm working on improving it. Basically I use the values pulled from localstorage into vuex to create a new user object in the vue component that has reference to the methods located in my user class declaration. I recalled recommendations that we should create clones of objects pulled from vuex for use within the vue component. I'm still refining the code but that's basic idea.

Aurelia - ValidationController always returns true

I've made a wizard with aurelia and I want to check the input values when the user hits the next button.
#autoinject()
export class settingsWizard {
steps: Step[];
activeStep: Step;
valController: ValidationController;
constructor(private i18n: I18N, private http: HttpClient, public wizard: Wizard, private log: Logger, private router: Router, private controllerFactory: ValidationControllerFactory) {
this.steps = [
new Step(1, this.i18n.tr("route1"), PLATFORM.moduleName('./steps/step1')),
new Step(2, this.i18n.tr("route2"), PLATFORM.moduleName('./steps/step2')),
new Step(3, this.i18n.tr("route3"), PLATFORM.moduleName('./steps/step3')),
new Step(4, this.i18n.tr("route4"), PLATFORM.moduleName('./steps/step4')),
];
ValidationRules
.ensure('availableTotalTime').required().on(this.wizard.plannedTime);
//.ensureObject()
//.satisfies((x: IQualityRate) => x.numberOfProducedQuantity > x.rejects)
//.withMessage(this.i18n.tr('quality-rate.rejectsHigherThanProducedQuantity'))
//.on(wizard.qualityRate);
this.valController = controllerFactory.createForCurrentScope();
this.valController.validateTrigger = validateTrigger.manual;
}
here's the nextButton event:
nextStep() {
this.valController.validate().then(result => {
if (result.valid) {
if (this.activeStep.id !== this.steps.length) {
this.activeStep = this.steps[this.activeStep.id];
}
}
});
}
My aurelia ValidationController always returns result.valid = true even though availableTotalTime is undefined. What am I doing wrong?
You might be assuming that the ValidationRules api automatically causes objects to be validated, but it only stores the rules on the object's prototype.
The ValidationController only validates objects which are either explicitly (1) or implicitly (2) registered, or directly provided (3) to it.
1. Register "implicitly"
Any property in a view that has the & validate binding behavior on it will be validated whenever you call controller.validate() in the same viewmodel.
2. Register explicitly
Likewise, controller.addObject(this.wizard.plannedTime) will cause that object (and all its properties) to be validated on subsequent calls to .validate()
3. Provide a ValidateInstruction
this.valController.validate({
object: this.wizard.plannedTime,
// optional: propertyName: ...,
// optional: rules: ...
});
This gives you the highest degree of control. It will only validate the object you pass in, and nothing else. Also it won't register the object, so subsequent calls to .validate() will not automatically validate it again.
You'll probably either want to go with option 2 or 3, depending on other concerns.
Ended up here as I was getting the same problem. Fred Kleuver's answer is correct, but only when using .on to register your view model (or in this case, the object with the property to be validated) when creating your rules.
If you are like me and you're explicitly passing in the validation rules into the binding behavior, like & validate:myRules (this happens to be how most of the examples I've seen online doing it) you will need to follow the Entity Validation portion of the documentation.That way you can explicitly pass your validation rules into your validationController.addObject call in the same way you would pass your rules to your binding behavior in your view.
this.validationRules = ValidationRules
.ensure('availableTotalTime')
.required()
.rules;
this.validationController.addObject(this.wizard.plannedTime, this.validationRules);

Can't get SignalR client events published with Aurelia Event Aggregator

I have a single page app based on Aurelia and I'm trying to get it to work with an existing SignalR backend. I've downloaded the SignalR javascript client and integrated it with the Aurelia app manually (i.e. I'm not using a proxy file). I'm able to connect to the SignalR hub and see the arrvive messages in the console.... so far so good. Now, I'm trying to use the Aurelia Event Aggregator so that when a new hub message arrives an event is fired and any components of the app subscribed to that particular event will do some work. The issue is that the SignalR event callback doesn't seem to be able to access the Event Aggregator object. Here's the code to illustrate the issue:
//Import statements omitted for brevity
#inject (EventAggregator)
export class MyService{
constructor(eventAggregator) {
this.ea = eventAggregator;
this.connection = $.hubConnection("http://localhost:8080/signalr", { useDefaultPath: false });
this.hub = this.connection.createHubProxy("myHub");
//Register a callback function to fire when a new hub message arrives
this.hub.on("sendMessage", this.processHubMessage);
//No issues so far - all this constructor code works fine
}
processHubMessage(message) {
// This doesn't work - this.ea is undefined in the scope of this function
this.ea.publish('deviceStatusUpdate', message);
}
}
The event aggregator object referenced within the callback function is not defined - I assume because it's not being called within the scope of the class. Is there a way to resolve this? How do I give the callback function access to the class properties (this.ea in my example).
Try using
this.hub.on("sendMessage", (message) => this.processHubMessage(message));
It's failing on you due to how this isn't what you're expecting it to be. By using a fat arrow function, this is what you expect it to be. This is a really frustrating part of JavaScript, but fat arrows provide a simple workaround for it.
I think you are missing the 'start' for your Proxy, also you may need to alias your view model to pass to the HubProxy.
This works for me:
constructor(eventAggregator){
this.eventAggregator = eventAggregator;
var signalrAddress = 'https://pathToYouServer';
var hubName = 'yourHubsName';
var connection = $.hubConnection(signalrAddress);
var eventHubProxy = connection.createHubProxy(hubName);
var vm = this;
eventHubProxy.on('yourBroadcastMessage', function(data) {
vm.eventAggregator.publish(data);
});
connection.start();
}

Variable in controller is lost when passing between views

I am currently stuck with an issue in my MVC 4 application. I have private variable in controller, that holds instance of a simple class:
private InstallationStatus status = null;
When data get submitted on a view, it gets filled like this:
InstallationStatus installStatus = Install();
if (installStatus != null)
{
status = installStatus;
TempData["installPercent"] = 0;
return View("InstallationProgress", status);
}
This part works as intended, variable is set to the instance as it should be.
After that view periodically checks another variable (using ajax):
<script type="text/javascript">
$(document).ready(function () {
var progress = 0;
$("div.status-message").text("Progress: " + progress + "%");
var statusUpdate = setInterval(function () {
$.ajax({
type: 'GET',
url: "/AppStart/GetInstallProgress",
datatype: "application/html; charset=utf-8",
success: function (data) {
progress = parseInt(data);
if (progress >= 100) {
clearInterval(statusUpdate);
var data = $(this).serialize();
$.ajax({
type: 'POST',
url: "#Url.Action("CompletedStatus", "AppStart")",
success: function () {
window.location = "/Login/Login"
}
});
}
$("div.status-message").text("Progress: " + progress + "%");
}
});
}, 2000);
});
</script>
When it calls "CompletedStatus" action on the controller, variable "status" on the controller is null (the instance set previously is not there?
How do I ensure that its value will persist? It seems to me like whole instance of controller gets lost, but that doesnt really matter to me - the source for "status" is webservice and once I get the instance of InstallationStatus, I cant get it again - I need to keep it.
I tried using TempData to store it but since there can be more than one step between storing it and retrieving it TempData proved unreliable.
The final process is:
Request installation status and navigate to view for installation progress (status will be received when progress will finish).
navigate to view where I will by updating installation progress
using javascript whenever I get callback from server with info about
progress
when installation finishes (status is returned) pass that status to
another view
In the example above I have some dummy code-behind, so the status is returned immediately, but that has no impact on the problem I have.
Currently I can do 1 and 2 and I can call the final view, but I cant pass the status in there because I dont have it on controller anymore and TempData are not reliable for this (sometimes it is still there, sometimes it is not).
Any help will be greatly appreciated.
When it calls "CompletedStatus" action on the controller, variable
"status" on the controller is null (the instance set previously is not
there?
How do I ensure that its value will persist?
private InstallationStatus status = null;
It won't unless it's a static value and that would be a very bad thing to do. Remember that variable values (private members' values) are only scoped within the http request. If you do another request then that's a totally whole new scope for your private variables.
I tried using TempData to store it but since there can be more than
one step between storing it and retrieving it TempData proved
unreliable.
That's because TempData will not have the value you expect it to have once you do another request. One good example of using this TempData is when you want to pass/move some values between a POST and GET, that is when you do a POST and do a redirect. TempData does not fit your case.
Now for a possible solution to your scenario, a good question is: is the installation process called once? Is it unique per user? If it is, which I highly suspect it is, then you need to uniquely identify each request. You can simply use a GUID to identify each request. Save that into your database (better than saving in a session) along with some other information like the status of the installation. Pass that guid back to your client and let them pass it back to the controller and retrieve an update on the status of the installation.

Dojo datagrid jsonrest response headers

I'd like to use custom headers to provide some more information about the response data. Is it possible to get the headers in a response from a dojo datagrid hooked up to a jsonRest object via an object store (dojo 1.7)? I see this is possible when you are making the XHR request, but in this case it is being made by the grid.
The API provides an event for a response error which returns the response object:
on(this.grid, 'FetchError', function (response, req) {
var header = response.xhr.getAllResponseHeaders();
});
using this I am successfully able to access my custom response headers. However, there doesn't appear to be a way to get the response object when the request is successful. I have been using the undocumented private event _onFetchComplete with aspect after, however, this does not allow access to the response object, just the response values
aspect.after(this.grid, '_onFetchComplete', function (response, request)
{
///unable to get headers, response is the returned values
}, true);
Edit:
I managed to get something working, but I suspect it is very over engineered and someone with a better understanding could come up with a simpler solution. I ended up adding aspect around to allow me to get hold of the deferred object in the rest store which is returned to the object store. Here I added a new function to the deffered to return the headers. I then hooked in to the onFetch of the object store using dojo hitch (because I needed the results in the current scope). It seems messy to me
aspect.around(restStore, "query", function (original) {
return function (method, args) {
var def = original.call(this, method, args);
def.headers = deferred1.then(function () {
var hd = def.ioArgs.xhr.getResponseHeader("myHeader");
return hd;
});
return def;
};
});
aspect.after(objectStore, 'onFetch', lang.hitch(this, function (response) {
response.headers.then(lang.hitch(this, function (evt) {
var headerResult = evt;
}));
}), true);
Is there a better way?
I solved this today after reading this post, thought I'd feed back.
dojo/store/JsonRest solves it also but my code ended up slightly different.
var MyStore = declare(JsonRest, {
query: function () {
var results = this.inherited(arguments);
console.log('Results: ', results);
results.response.then(function (res) {
var myheader = res.xhr.getResponseHeader('My-Header');
doSomethingWith(myheader);
});
return results;
}
});
So you override the normal query() function, let it execute and return its promise, and attach your own listener to its 'response' member resolving, in which you can access the xhr object that has the headers. This ought to let you interpret the JsonRest result while fitting nicely into the chain of the query() all invokers.
One word of warning, this code is modified for posting here, and actually inherited from another intermediary class that also overrode query(), but the basics here are pretty sound.
If what you want is to get info from the server, also a custom key-value in the cookie can be a solution, that was my case, first I was looking for a custom response header but I couldn't make it work so I did the cookie way getting the info after the grid data is fetched:
dojo.connect(grid, "_onFetchComplete", function (){
doSomethingWith(dojo.cookie("My-Key"));
});
This is useful for example to present a SUM(field) for all rows in a paginated datagrid, and not only those included in the current page. In the server you can fetch the COUNT and the SUM, the COUNT will be sent in the Content-Range header and the SUM can be sent in the cookie.