Breezejs flips the EntityState from Added to Modified before Save - breeze

I am building a SPA per the guidance provided in John Papa's Jumpstart.
When I create the model, it has
modelObservable().entityAspect.entityState.isAdded() = true;
I update the text, dropdown and
modelObservable().entityAspect.entityState.isAdded() = false;
in my Datacontext:
var createProject = function (position) {
return manager.createEntity(entityNames.project,
{
positionId : position.id(),
start : position.start(),
memberId : position.memberId()
});
};
which is called from my add viewModel:
define(['services/datacontext', 'durandal/plugins/router', 'durandal/system', 'durandal/app', 'services/logger', 'services/uiService'],
function (datacontext, router, system, app, logger, ui) {
var model = ko.observable();
var position = ko.observable();
var hourTypes = ko.observableArray([]);
var isSaving = ko.observable(false);
// init
var activate = function (routeData) {
logger.log('Add View Activated', null, 'add', true);
var positionId = parseInt(routeData.id);
initLookups();
return datacontext.getPositionById(positionId, position).then(**createProject**);
};
var initLookups = function () {
logger.log('initLookups', null, 'add', true);
hourTypes(datacontext.lookups.hourTypes);
};
// state
**var createProject = function () {
return model(datacontext.createProject(position()));
}**
var addNewProject = function () {
if (position == undefined || position().id() < 1) {
console.log('callback addNewProject');
setTimeout(function () {
addNewProject();
}, 1000);
} else {
datacontext.addProject(position(), model);
console.log(model().id());
return;
}
}
var **save** = function () {
isSaving(true);
**datacontext.saveChanges()**
.then(goToEditView).fin(complete);
function complete() {
isSaving(false);
}
function goToEditView() {
isSaving(false);
var url = '#/Projects/';
router.navigateTo(url + model().id());
}
};
var vm = {
activate: activate,
hourTypes: hourTypes,
isAdded: isAdded,
model: model,
save: save,
title: 'Details View'
};
return vm;
});
the html
<section data-bind="with:model">
<h1 data-bind="text: name"> <i class="icon-asterisk" data-bind="visible: hasChanges" style="font-size: 30px;"></i></h1>
<div class="errorPanel"></div>
<div id="overview" class="project" >
<div class="row">
<div class="span4">
<label class="requiredLabel">Name*</label>
<input type="text" name="name" data-bind="value: name" style="width: 27em;" class="required" placeholder="Project Name" required validationMessage="Project Name required" /><span class="k-invalid-msg" data-for="title"></span>
</div>
</div>
<div class="row">
<div class="span3"><label class="requiredLabel">Start*</label></div>
<div class="span3"><label class="requiredLabel">End</label></div>
</div>
<div class="row">
<div class="span3"><input name="start" data-bind="shortDate: start" class="date required" required="required" placeholder="mm/dd/yyyy" style=" width:142px"></div>
<div class="span3"><input name="end" data-bind="shortDate: end" class="date" placeholder="mm/dd/yyyy" style=" width:142px"><span class="k-invalid-msg" data-for="end"></span></div>
</div>
<br/>
<div class="row">
<div class="span3"><label for="hourType" class="requiredLabel">Measure As*</label></div>
<div class="span2"><label for="hoursPerWeek" class="requiredLabel">Hours/Week</label></div>
<div class="span2"><label for="totalHours" class="requiredLabel">Total Hours</label></div>
</div>
<div class="row">
<div class="span3">
<select id="hourType" data-bind="options: $parent.hourTypes, optionsText: 'name', value: hourType" required validationMessage="Measure As required"></select><span class="k-invalid-msg" data-for="hourType"></span>
</div>
<div class="span2">
<input name="hoursPerWeek" type="number" min="1" max="120" required="required" data-bind="value: hoursPerWeek, validationOptions: { errorElementClass: 'input-validation-error' }, enable: hourType().id() == 1" class="hours required"" style="width: 80px;" validationMessage="Hours required"><span class="k-invalid-msg" data-for="projectHours"></span>
<span class="k-invalid-msg" data-for="totalHours"></span>
</div>
<div class="span2">
<input name="totalHours" type="number" min="40" max="2080" required="required" data-bind="value: totalHours, validationOptions: { errorElementClass: 'input-validation-error' }, enable: hourType().id() == 2" class="hours required"" style="width: 80px;" validationMessage="Hours required"><span class="k-invalid-msg" data-for="projectHours"></span>
<span class="k-invalid-msg" data-for="totalHours"></span>
</div>
</div>
<div class="row">
<div class="span4">
<label class="requiredLabel">Description*</label><span class="k-invalid-msg" data-for="description"></span><span id="posMinDesc" style="visibility:hidden"></span>
<textarea id="description" name="description" style="height: 200px; width: 650px;" data-bind="value: description, enabled:true, click: $parent.clearDefaults" rows="4" cols="60" class="richTextEditor k-textbox" required validationMessage="Description required" ></textarea>
</div>
</div>
</div>
<div class="button-bar">
<button class="btn btn-info" data-bind="click: $parent.goBack"><i class="icon-hand-left"></i> Back</button>
<button class="btn btn-info" data-bind="click: $parent.save, enable: $parent.canSave"><i class="icon-save"></i> Save</button>
</div>
</section>
The json breeze sends to my controller is this:
{
"entities": [
{
"Id": -1,
"Description": "poi",
"End": null,
"Gauge": 0,
"Score": 0,
"HourTypeId": 1,
"HoursPerWeek": 45,
"HourlyRate": null,
"TotalHours": null,
"WeightedHours": 0,
"CreditMinutes": 0,
"TotalCompensation": null,
"IsCurrent": false,
"Name": "poi",
"PositionId": 1,
"MemberId": 1,
"Start": "2011-09-01T00:00:00Z",
"undefined": false,
"entityAspect": {
"entityTypeName": "Project:#SkillTraxx.Model",
"defaultResourceName": "Projects",
"entityState": "Modified",
"originalValuesMap": {
"Name": "",
"HourTypeId": 0,
"HoursPerWeek": null,
"Description": ""
},
"autoGeneratedKey": {
"propertyName": "Id",
"autoGeneratedKeyType": "Identity"
}
}
}
],
"saveOptions": {}
}
As you can see, the above is incorrect b/c state is "Modified" and the Id = -1. This throws an error server side. I suppose I could trap the DbUpdateConcurrencyException, unwind the JObject and change "Modified" to added, but that's got code smell all over it.
If anyone can help me find the face-palm moment in all of this, I'm ready.
Thanks for looking!

FACE PALMED IT
I took Jays advice and started stripping away the html then I realize it was my handler.
The update method on shortDate handler was responsible. I wrapped it in an if statement not to send the update if the current state is added.
ko.bindingHandlers.shortDate = {
init: function (element, valueAccessor) {
//attach an event handler to our dom element to handle user input
element.onchange = function () {
var value = valueAccessor();//get our observable
//set our observable to the parsed date from the input
value(moment(element.value).toDate());
};
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var value = valueAccessor();
var valueUnwrapped = ko.utils.unwrapObservable(value);
if (valueUnwrapped) {
element.value = moment(valueUnwrapped).format('L');
if (!viewModel.entityAspect.entityState.isAdded())
{
**viewModel.entityAspect.setModified();**
}
}
}
};

Related

Vue.js and Form array

I am converting a form from knockout to vue 2 with an ASP.NET MVC 5 Controller for the back end. This is my first attempt using vue. When the user presses the + button, it adds a new row onto the form. The issue I'm having is submitting that to the back end. The old knockout code dynamically set the name attribute in the form elements to Quantities so the form array properly submits all of the data. I would like to do the same thing with vue but am having a a bit of trouble dynamically setting the name of each element dynamically. What would be the best way to do this in order to submit the form without having to rewrite the Controller signature? It should store the form array under the name Quantities.
Image:
HTML:
<div id="radios" v-for="(row,key) in order">
<div class="row">
<div class="col-md-3">
#Html.SmartLabel("systemType", "System Type", true, labelClass)
<label>
<input type="radio" value="#Model.WifiId" v-model="order[key].selectedSystemType" v-on:change="systemChanged(key)"> Wifi
</label>
<label>
<input type="radio" value="#Model.WirelessId" v-model="order[key].selectedSystemType" v-on:change="systemChanged(key)"> Wireless
</label>
<label>
<input type="radio" value="#Model.FiberId" v-model="order[key].selectedSystemType" v-on:change="systemChanged(key)"> Fiber
</label>
</div>
<div class="col-md-4">
#Html.SmartLabel("assetType", "Asset Type", true, labelClass)
<!--
https://stackoverflow.com/questions/43812817/how-to-set-optgroup-select-label-in-vue-js
-->
<select id="asset-type" name="" class="form-control" v-model="order[key].selectedAsset" v-on:change="assetTypeChanged(key)">
<optgroup v-for="(group, name) in order[key].assets" v-bind:label="name">
<option v-for="asset in group" v-bind:value="asset">
{{asset.Name}}
</option>
</optgroup>
</select>
</div>
<div class="col-md-2">
#Html.SmartLabel("quantity", "Quantity", true, labelClass)
<input type="number" min="1" class="form-control" v-model="order[key].quantity" />
</div>
<div class="col-md-2">
<div v-show="costTracked">
#Html.SmartLabel("cost", "Unit Cost", true, labelClass)
<input type="number" min="1" class="form-control" v-model="order[key].cost" />
</div>
</div>
<div>
<span style="cursor:pointer" v-on:click="removeItem(key)">
X
</span>
</div>
</div>
</div>
Vue.js code:
var app = new Vue({
el: '#vue-app',
data: {
assets: {},
assetQuantityEnabled: false,
costTracked: false,
order: []
//Quantities: []
},
methods: {
// Trigger when system type radio selection changes
systemChanged: function (key) {
$.getJSON('#Url.Action( "GetAssetsOfSystem", "RadioOrder" )?systemTypeId=' + this.order[key].selectedSystemType, function (data) {
var result = {};
// Add assets to category (AssetCategory)
for (const asset of data) {
(asset.AssetCategoryName in result) ? result[asset.AssetCategoryName].push(asset) : result[asset.AssetCategoryName] = [];
}
// Sort assets within each category
for (const option in result) {
result[option].sort((a, b) => {
return a.Name.localeCompare(b.Name);
});
}
this.order[key].assets = result;
this.costTracked = false;
this.assetQuantityEnabled = false;
}.bind(this));
},
assetTypeChanged: function (key) {
this.assetQuantityEnabled = true;
this.costTracked = this.hasMacOrSerial(this.order[key].selectedAsset);
},
addItem: function () {
obj = this.defaultObj();
this.order.push(obj);
},
convertFormData: function(){
var q = [];
for(let i=0; i < this.order.length; i++)
{
var obj = {
AssetTypeId: this.order[i].selectedAsset.AssetTypeId,
AssetTypeIsSerialized: this.hasMacOrSerial(this.order[i].selectedAsset),
Quantity: this.order[i].quantity,
UnitCost: this.order[i].cost
};
q.push(obj);
}
return q;
},
defaultObj: function () {
return {
asset: '',
assets: [], // list items
cost: 0,
quantity: 1,
selectedSystemType: null,
selectedAsset: null
};
},
hasMacOrSerial: function (asset) {
return (asset.HasMacAddress || asset.HasSerialNumber) ? true : false;
},
removeItem: function (key) {
this.order.splice(key, 1);
}
}
});
Controller method signature:
public virtual ActionResult Create(
[Bind(Include = "VendorId,OrderNumber,RequisitionNumber,Quantities,SerializedAssets,Attempt")] InventoryOrderDto dto)
Old HTML using Knockout:
<div class="col-md-6">
<div class="panel panel-default white-box">
<div class="panel-heading">
<h3 class="panel-title">Order Summary</h3>
</div>
<div class="panel-body">
<div id="radios" data-bind="foreach: QuantitiesDisplay, visible: QuantitiesDisplay().length > 0" style="display: none">
<div class="row">
<input type="hidden" data-bind="value: AssetTypeId, attr: {name: 'Quantities[' + $index() + '].AssetTypeId'}" />
<input type="hidden" data-bind="value: AssetTypeIsSerialized, attr: {name: 'QuantitiesDisplay[' + $index() + '].AssetTypeIsSerialized'}" />
<input type="hidden" data-bind="value: UnitCost, attr: {name: 'Quantities[' + $index() + '].UnitCost'}" />
<div class="col-md-3" data-bind="text: AssetTypeName" style=" word-break: break-all;">
</div>
<div class="col-md-3">
<input type="number" min="1" class="form-control" data-bind="value: Quantity, attr: {name: 'Quantities[' + $index() + '].Quantity'}" />
</div>
<div class="col-md-3">
<input type="number" min="1" class="form-control" data-bind="visible:AssetTypeIsSerialized, value: UnitCost, attr: {name: 'Quantities[' + $index() + '].UnitCost'}" />
</div>
<div class="col-md-2">
<a class="btn btn-danger" title="Remove" data-bind="click: $root.removeItem">
<i class="fa fa-minus-circle"></i> Remove
</a>
</div>
</div>
</div>
<div id="emptyOrder" data-bind="visible: QuantitiesDisplay().length == 0">
No items are currently selected. Please choose from the adjacent pane.
</div>
<input id="submit" type="submit" value="Continue" class="btn btn-primary" data-bind="visible: QuantitiesDisplay().length > 0" style="display: none">
</div>
</div>
</div>
Ah I found it - need to use v-bind to dynamically set the name.
<div class="col-md-2">
#Html.SmartLabel("quantity", "Quantity", true, labelClass)
<input type="number" min="1" class="form-control" v-model="order[key].quantity" v-bind:name="'Quantities[' + key + '].Quantity' " />
</div>
https://medium.com/swlh/building-dynamic-forms-with-django-formsets-and-vue-js-f3c6e2dddd4a

How to perform a login identification process in the best way?

I have 2 inputs boxes: Public key (user name) and Private key (password). Also I have a hidden warning label.
This is my code (and it's perfectly working ):
<script language="javascript">
function SendLoginData() {
document.getElementById("LoginErrorLabel").style.display = "none";
var url = "/DappAccount/CheckAccount";
$.post(url, { PublicKey: $("#public_key_input").val(), PrivateKey: $("#private_key_input").val() }, function (data) {
if (data == false) {
document.getElementById("LoginErrorLabel").style.display = "block";
return;
}
else {
document.getElementById("loader").style.display = "block";
$("#myform").submit()
}
});
}
</script>
<div class="container">
#using (Html.BeginForm("AccountMainPage", "DappAccount", FormMethod.Post, new { #id = "myform" }))
{
<div class="row justify-content-center">
#Html.TextBoxFor(m => m.publicKey, new { id = "public_key_input", placeholder = "Ethereum Public Key", required = "required" })
</div> <br />
<div class="row justify-content-center">
#Html.TextBoxFor(m => m.privateKey, new { id = "private_key_input", placeholder = "Ethereum Private Key", required = "required", type = "password" })
</div> <br />
<div class="row justify-content-center">
<img id="loader" style="display: none;" src="https://s5.gifyu.com/images/Loader5a73d3b26568dbc4.gif" alt="Loader5a73d3b26568dbc4.gif" border="0" />
</div> <br />
}
<div class="row justify-content-center">
<input id="submit" type="button" value="Login" class="btn btn-primary" onclick="SendLoginData()" />
</div>
<label id="LoginErrorLabel" style="color: red; display: none;">*Wrong login detail !</label>
</div>
What I'm doing here is:
1) Send username&password to the 'CheckAccount' method in the controller, the method returns true/false.
2) If false, show label,
if true, show gif image and send again the details to ActionResult (=AccountMainPage) which returns a new view.
I just wonder, is there a better/shorter way to do it using one post or one method? I heard something about the partial views in MVC, no idea what to do with it. People told me that what I did here is too old for MVC

How to update input element using selected value from bootstrap dropdown

I would like to use knockout for updating dynamically the input element by the selected value from the dropdown but I don't understand how to use the code I commented here bellow with the dropdown and I'm not sure if it's the right way to do... Please help me understand and fix this problem.
View:
<div class="form-group">
<label for="dropDownRelationType">Business relation type</label>
<div class="input-group">
<input type="text" class="form-control" id="txtRelationTypeId" name="txtRelationTypeId" data-bind="value: companyModel.relationTypeId" placeholder="Business relation type"
<div class="input-group-btn">
<button type="button" id="dropDownRelationType" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true">
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" data-bind="foreach: relationTypes">
<li><a role="menuitem" href="#" data-bind="text: relationTypeId"></a></li>
</ul>
</div>
</div>
</div>
Knockout:
function companyModel() {
var self = this;
self.companyName = ko.observable("");
self.status = ko.observable("1");
self.relationTypeId = ko.observable("");
self.relationTypes = ko.observableArray();
}
function CompanyViewModel() {
var self = this;
self.companyModel = new companyModel();
self.relationTypes = ko.observableArray([
{ relationTypeId: "1" }, { relationTypeId: "2" }, { relationTypeId: "3" }, { relationTypeId: "4" }
])
//-------------------------------------------------------
//self.relationTypes.subscribe(function (selectedRelationTypeId) {
// shouter.notifySubscribers(selectedRelationTypeId, "valueToPublish");
//});
//shouter.subscribe(function (selectedRelationTypeId) {
// self.relationTypeId(selectedRelationTypeId);
//}, self, "valueToPublish");
//-------------------------------------------------------
}
var companyViewModel = new CompanyViewModel();
$(function () {
ko.applyBindings(companyViewModel);
});
You can add an observable to select the relationTypeId like
function CompanyViewModel() {
var self = this;
self.companyModel = new companyModel();
self.selectedRelationTypeId = ko.observable();
self.relationTypes = ko.observableArray([
{ relationTypeId: "1" }, { relationTypeId: "2" }, { relationTypeId: "3" }, { relationTypeId: "4" }
])
self.selectRelationTypeId = function(relationType){
self.selectedRelationTypeId(relationType.relationTypeId);
};
}
and change your html markup as
<div class="form-group">
<label for="dropDownRelationType">Business relation type</label>
<div class="input-group">
<input type="text" class="form-control" id="txtRelationTypeId" name="txtRelationTypeId" data-bind="value: selectedRelationTypeId" placeholder="Business relation type" />
<div class="input-group-btn">
<button type="button" id="dropDownRelationType" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-haspopup="true">
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu" data-bind="foreach: relationTypes">
<li><a role="menuitem" href="#" data-bind="text: relationTypeId, click: $root.selectRelationTypeId"></a></li>
</ul>
</div>
</div>
</div>
You should be able to see the selection you make in the input field.

Automatically update viewModel after save to db

What I want to achieve is once the data got saved into database, when it goes back to client, it will automatically update the observable array. But somehow I couldn't make it happen.
This is my Server side code:
[HttpGet]
public JsonResult GetTasks()
{
var tasks = context.ToDoTasks.ToList();
return Json(tasks.Select(c => new TaskViewModel(c)).ToList(), JsonRequestBehavior.AllowGet);
}
[HttpPost]
public JsonResult AddTask(string text, string date)
{
var nTask = new ToDoTask()
{
Text = text,
Date = DateTime.ParseExact(date, "MM/dd/yyyy", System.Globalization.CultureInfo.InvariantCulture),
IsDone = false,
Order = 1,
};
context.ToDoTasks.Add(nTask);
context.SaveChanges();
return Json(new TaskViewModel(nTask), JsonRequestBehavior.AllowGet);
}
This is my cshtml file code:
<form>
<div class="controls controls-row" style="margin-top:40px;">
<input class="span7" type="text" placeholder="Task to do" style="margin-right:4px;" id="oText">
<div id="task-date" class="input-append date">
<input data-format="MM/dd/yyyy" type="text" placeholder="MM/dd/yyyy" name="taskDate" id="oDate" />
<span class="add-on">
<i data-time-icon="icon-time" data-date-icon="icon-calendar">
</i>
</span>
</div>
<button class="btn" type="submit" style="margin-top:-10px;" data-bind="click: save">+</button>
</div>
<div class="controls">
<label class="checkbox">
<input type="checkbox"> Mark all as complete
</label>
</div>
<div id="task-section" style="margin-top:20px;">
<ul data-bind="foreach: Tasks">
<!-- ko if: IsDone -->
<li>
<span><input type="checkbox" style="margin:-5px 5px 0px 0px;" data-bind="checked: IsDone" /></span>
<del><span data-bind="text: Text"></span></del>
<del><span class="task-date" data-bind="text: Date"></span></del>
</li>
<!-- /ko -->
<!-- ko ifnot: IsDone -->
<li>
<span><input type="checkbox" style="margin:-5px 5px 0px 0px;" data-bind="checked: IsDone" /></span>
<span data-bind="text: Text"></span>
<span class="task-date" data-bind="text: Date"></span>
</li>
<!-- /ko -->
</ul>
</div>
<div class="clearfix" style="margin-top:30px;">
<span class="pull-left" style="font-weight:bold;"><span data-bind="text: oItemLeft"></span> item left</span>
<span class="pull-right badge" style="cursor:pointer;" data-bind="click: remove">Clear # completed item</span>
</div>
</form>
And finally my JS:
var ViewModel = function (data) {
var self = this;
self.Tasks = ko.mapping.fromJS(data, {}, self.Tasks);
self.oItemLeft = ko.computed(function () {
var i = 0;
data.forEach(function (entry) {
if (!entry.IsDone) i++;
});
return i;
});
self.save = function () {
$.ajax({
url: "Home/AddTask",
type: "POST",
data: { text: $('#oText').val(), date: $('#oDate').val() },
success: function (response) {
ko.mapping.fromJS(response, ViewModel);
}
});
};
self.remove = function () {
alert('delete');
}
}
$(function () {
$.getJSON("/Home/GetTasks/", null, function (data) {
ko.applyBindings(new ViewModel(data));
});
// for datepicker
$('#task-date').datetimepicker({
language: 'pt-BR',
pickTime: false
});
});
self.save = function () {
$.ajax({
url: "Home/AddTask",
type: "POST",
data: { text: $('#oText').val(), date: $('#oDate').val() },
success: function (response) {
var task = ko.mapping.fromJS(response);
self.Tasks.push(task);
}
});
};
Also for oItemLeft you should be referring to self.Tasks instead of data:
self.oItemLeft = ko.computed(function () {
var i = 0;
self.Tasks().forEach(function (entry) {
if (!entry.IsDone) i++;
});
return i;
});

entityAspect.setDeleted doesn't fire the subscribed propertyChanged event

I am running into problem where i subscribe to propertyChanged event, the subscribed event does fire for entity Modification, but never fires for when setting entity to Deleted.
what might i be doing wrong.
The objective of what i am doing is that, whenever user modifies the row, i want to provide
button at row level to cancel the changes. similarly when user deletes a row, i want to provide a button to unDelete a row. The modification part works as expected, but for Delete it is not working.
I was Expecting that item.entityAspect.setDeleted(), would fire the propertyChanged Event
so that i can update the vlaue of observable IsDeleted,which in turn would update the visibility of the button.
ViewModel
/// <reference path="jquery-1.8.3.js" />
/// <reference path="../linq-vsdoc.js" />
/// <reference path="../linq.min.js" />
/// <reference path="../breeze.intellisense.js" />
/// <reference path="../breeze.debug.js" />
$(document).ready(function () {
//extend country type
var Country = function () {
console.log("Country initialized");
var self = this;
self.Country_ID = ko.observable(0); // default FirstName
self.Country_Code = ko.observable(""); // default LastName
self.Country_Name = ko.observable("");
self.entityState = ko.observable("");
self.hasValidationErrors = ko.observable(false);
self.IsDeleted = ko.observable(false);
self.IsModified = ko.observable(false);
self.templateName = ko.observable("AlwayEditable");
var onChange = function () {
var hasError = self.entityAspect.getValidationErrors().length > 0;
if (hasError)
self.hasValidationErrors(true);
else
self.hasValidationErrors(false);
};
//dummy property to wireup event
//should not be used for any other purpose
self.hasError = ko.computed(
{
read: function () {
self.entityAspect // ... and when errors collection changes
.validationErrorsChanged.subscribe(onChange);
},
// required because entityAspect property will not be available till Query
// return some data
deferEvaluation: true
});
//dummy property to wireupEvent and updated self.entityStateChanged property
self.entityStateChanged = ko.computed({
read: function () {
self.entityAspect.propertyChanged.subscribe(function (changeArgs) {
if (changeArgs.entity.entityAspect.entityState.name == "Deleted") {
self.IsDeleted(false);
}
else if (changeArgs.entity.entityAspect.entityState.name == "Modified")
self.IsModified(true);
}); //subscribe
},
deferEvaluation: true,
// self.entityStateChanged(false)
});
self.fullName = ko.computed(
function () {
return self.Country_Code() + " --- " + self.Country_Name();
});
};
manager.metadataStore.registerEntityTypeCtor("Country", Country);
var countryViewModel = function (manager) {
var self = this;
window.viewModel = self;
self.list = ko.observableArray([]);
self.pageSize = ko.observable(2);
self.pageIndex = ko.observable(0);
self.selectedItem = ko.observable();
self.hasChanges = ko.observable(false);
self.totalRows = ko.observable(0);
self.totalServerRows = ko.observable(0);
self.RowsModified = ko.observable(false);
self.RowsAdded = ko.observable(false);
self.RowsDeleted = ko.observable(false);
self.templateToUse = function (dataItem, context) {
var item = dataItem;
if (!_itemTemplate) {
_itemTemplate = ko.computed(function (item) {
//var x = this;
if (this.entityAspect == "undefined")
return this.templateName("AlwayEditable");
if (this.entityAspect.entityState.name == "Deleted") {
this.templateName("readOnlyTmpl");
return this.templateName();
}
else {
this.templateName("AlwayEditable");
return this.templateName();
}
}, item);
}
if (item.entityAspect.entityState.name == "Deleted") {
item.templateName("readOnlyTmpl");
return item.templateName();
}
else {
item.templateName("AlwayEditable");
return item.templateName();
}
// return _itemTemplate();
}
var _itemTemplate;
self.hasError = ko.computed(
{
read: function () {
self.entityAspect // ... and when errors collection changes
.validationErrorsChanged.subscribe(onChange);
},
// required because entityAspect property will not be available till Query
// return some data
deferEvaluation: true
});
self.acceptChanges = function (item) {
// self.selectedItem().entityAspect.acceptChanges();
self.selectedItem(null);
}
manager.hasChanges.subscribe(function (newvalue) {
self.hasChanges(newvalue.hasChanges);
});
self.edit = function (item, element) {
highlightRow(element.currentTarget, item);
self.selectedItem(item);
};
self.discardChanges = function () {
manager.rejectChanges();
manager.clear();
self.pageIndex(0);
self.loadData();
};
self.cancel = function (item, element) {
item.entityAspect.rejectChanges();
self.selectedItem(null);
};
self.add = function () {
var countryType = manager.metadataStore.getEntityType("Country"); // [1]
var newCountry = countryType.createEntity(); // [2]
//if not using this line, the table is not updated to show this newly added item
self.list.push(newCountry);
manager.addEntity(newCountry); // [3]
self.selectedItem(newCountry);
};
self.remove = function (item) {
item.entityAspect.rejectChanges();
item.entityAspect.setDeleted(); //was expecting that propertychaged subscribe event will fire, but it does not
item.templateName("readOnlyTmpl"); //if i don't do this the template is not changed/updated
item.IsDeleted(true); //have to use this
};
self.UndoDelete = function (item) {
item.entityAspect.rejectChanges();
item.templateName("AlwayEditable");
item.IsDeleted(false);
};
self.save = function () {
if (manager.hasChanges()) {
alertTimerId = setTimeout(function () {
//this works as well
$.blockUI({ message: '<img src="Images/360.gif" /> </p><h1>Please Saving Changes</h1>', css: { width: '275px' } });
}, 700);
manager.saveChanges()
.then(saveSucceeded(alertTimerId))
.fail(saveFailed);
} else {
$.pnotify({
title: 'Save Changes',
text: "Nothing to save"
});
// alert("Nothing to save");
};
};
manager.hasChanges.subscribe(function (newvalue) {
self.hasChanges(newvalue.hasChanges);
});
manager.entityChanged.subscribe(function (changeArg) {
self.RowsDeleted(manager.getEntities(null, [breeze.EntityState.Deleted]).length);
self.RowsModified(manager.getEntities(null, [breeze.EntityState.Modified]).length);
self.RowsAdded(manager.getEntities(null, [breeze.EntityState.Added]).length);
});
//we want maxPageIndex to be recalculated as soon as totalRows or pageSize changes
self.maxPageIndex = ko.dependentObservable(function () {
return Math.ceil(self.totalRows() / self.pageSize()) - 1;
//return Math.ceil(self.list().length / self.pageSize()) - 1;
});
self.previousPage = function () {
if (self.pageIndex() > 1) {
self.pageIndex(self.pageIndex() - 1);
//self.loadData();
getData();
}
};
self.nextPage = function () {
if (self.pageIndex() < self.maxPageIndex()) {
self.pageIndex(self.pageIndex() + 1);
// self.loadData();
getData();
}
};
self.allPages = ko.dependentObservable(function () {
var pages = [];
for (i = 0; i <= self.maxPageIndex() ; i++) {
pages.push({ pageNumber: (i + 1) });
}
return pages;
});
self.moveToPage = function (index) {
self.pageIndex(index);
//self.loadData();
getData();
};
};
// self.loadData
var vm = new countryViewModel(manager);
//ko.validation.group(vm);
ko.applyBindings(vm);
// ko.applyBindingsWithValidation(vm);
vm.loadData();
try {
} catch (e) {
displayModalMessage("Page Error :- Reload the Page", e.message);
}
}); //end document.ready
View
<p><a class="btn btn-primary" data-bind="click: $root.add" href="#" title="Add New Country"><i class="icon-plus"></i> Add Country</a></p>
<span> Search Country Code :</span><input id="txtSearch" type="text" /><input id="BtnSearch" type="button" value="Search" data-bind="click: $root.loadData" />
<!--<table class="table table-striped table-bordered " style="width: 700px">-->
<!--<table id="myTable" class="ui-widget" style="width: 800px">-->
<table id="myTable" class="table table-striped table-bordered " style="width: 1200px">
<caption> <div>
<span class="label label-info">Number of Rows Added </span> <span class="badge badge-info" data-bind="text: RowsAdded"></span> ,
<span class="label label-success">Number of Rows Modified</span> <span class="badge badge-success" data-bind="text: RowsModified"></span> ,
<span class="label label-important">Number of Rows Deleted</span> <span class="badge badge-important" data-bind="text: RowsDeleted"></span>
<p/>
</div></caption>
<thead class="ui-widget-header">
<tr>
<th>Code</th>
<th>Name</th>
<th>Full Name</th>
<th />
</tr>
</thead>
<!--<tbody data-bind=" title:ko.computed(function() { debugger; }), template:{name:templateToUse, foreach: list, afterRender: HighlightRows }" class="ui-widget-content"></tbody>-->
<tbody data-bind=" title:ko.computed(function() { debugger; }), template:{name:templateToUse, foreach: list}" ></tbody>
</table>
<div class="pagination">
<ul><li data-bind="css: { disabled: pageIndex() === 0 }">Previous</li></ul>
<ul data-bind="foreach: allPages">
<li data-bind="css: { active: $data.pageNumber === ($root.pageIndex() + 1) }"></li>
</ul>
<ul><li data-bind="css: { disabled: pageIndex() === maxPageIndex() }">Next</li></ul>
</div>
<!--<input id="Button1" type="button" value="Save" data-bind="attr: { disabled: !hasChanges()}, click:saveChanges" />-->
<a class="btn btn-success btn-primary" data-bind="click: $root.save, css: { disabled: !$root.hasChanges()}" href="#" title="Save Changes"> Save Changes</a>
<!-- <input id="Button3" type="button" value="Create New" data-bind="click:AddNewCountry" />
<input id="Button4" type="button" value="Discard and reload data" data-bind="click:discardreload, attr: { disabled: !hasChanges()}" /> -->
<a class="btn btn-danger btn-primary" data-bind="click: $root.discardChanges, css: { disabled: !$root.hasChanges()}" href="#" title="Discard Changes"><i class="icon-refresh"></i> Discard & Reload</a>
<script id="readOnlyTmpl" type="text/html">
<tr >
<td>
<span class="label " data-bind="text: Country_Code "></span>
<div data-bind="if: hasValidationErrors">
<span class="label label-important" data-bind="text: $data.entityAspect.getValidationErrors('Country_Code')[0].errorMessage ">Important</span>
</div>
</td>
<td>
<span class="label " data-bind="text: Country_Name "></span>
<p data-bind="validationMessage: Country_Name"></p>
<span data-bind='visible: ko.computed(function() { debugger; }), text: Country_Name.validationMessage'> </span>
</td>
<td> <span class="label " data-bind="text: fullName "></span>
</td>
<td >
<a class="btn btn-danger" data-bind="click: $root.cancel, visible: $data.IsModified" href="#" title="cancel/undo changes">Undo Changes<i class="icon-trash"></i></a>
<a class="btn btn-danger" data-bind="click: $root.remove, visible: !$data.IsDeleted() " href="#" title="Delete this Row">Delete<i class="icon-remove"></i></a>
<a class="btn btn-danger" data-bind="click: $root.UndoDelete, visible: $data.IsDeleted() " href="#" title="Undo Delete">Un Delete<i class="icon-remove"></i></a>
</td>
</tr>
</script>
<script id="AlwayEditable" type="text/html">
<tr >
<td><input type="text" placeholder="Country Code" data-bind="value: Country_Code , uniqueName: true, css: { error: hasValidationErrors }, valueUpdate: 'afterkeydown'"/>
<!-- <div data-bind="if: $data.entityAspect.getValidationErrors().length>0">
<pre data-bind="text: $data.entityAspect.getValidationErrors('Country_Code')[0].errorMessage "></pre>
</div>-->
<div data-bind="if: hasValidationErrors">
<span class="label label-important" data-bind="text: $data.entityAspect.getValidationErrors('Country_Code')[0].errorMessage ">Important</span>
</div>
</td>
<td><input type="text" placeholder="Country Name" data-bind="value: Country_Name, uniqueName: true, valueUpdate: 'afterkeydown'"/>
<p data-bind="validationMessage: Country_Name"></p>
<span data-bind='visible: ko.computed(function() { debugger; }), text: Country_Name.validationMessage'> </span>
</td>
<td>
<span data-bind=' text: fullName'> </span>
</td>
<td >
<a class="btn btn-danger" data-bind="click: $root.cancel, visible: $data.IsModified" href="#" title="cancel/undo changes">Undo Changes<i class="icon-trash"></i></a>
<a class="btn btn-danger" data-bind="click: $root.remove, visible: !$data.IsDeleted() " href="#" title="Delete this Row">Delete<i class="icon-remove"></i></a>
<a class="btn btn-danger" data-bind="click: $root.UndoDelete, visible: $data.IsDeleted() " href="#" title="Undo Delete">Un Delete<i class="icon-remove"></i></a>
</td>
</tr>
</script>
Analysis
The propertyChanged event is raised when ... a property changes. But that's not what you want to watch. You want to monitor the entityAspect.entityState
When you set a property to a new value (for example person.FirstName("Naunihal")), you get both a propertyChanged event and a change to the entity's EntityState.
When you delete the entity, the entity's EntityState changes ... to "Deleted". But deleting doesn't change a property of the entity. Breeze does not consider the EntityState itself to be a property of the entity. Therefore, there is no propertyChanged notification.
Solution
Update Jan 12, 2013
I think more people will discover this solution if I rephrase the question that you asked so people understand that you want to listen for changes to EntityState.
So I moved my answer to a new SO question: "How can I detect a change to an entity's EntityState?". Hope you don't mind following that link.

Resources