OData error when bind to an element in a Master-Detail app - sapui5

I have developed a Master-Detail app. In the Detail view, I am using a DynamicPage where within content, I am using an IconTabBar Element with 3 items. Each item is a different Fragment with a Smartform.
Master view is loading the data from an EntitySet (MasterEntity)
Detail view with a is using some fields from the Entity MasterEntity
Fragment 1: is using DetailEntitySet
Fragment 2: is using DetailEntitySet
Fragment 3: is using DetailEntitySet
Basically, when Detail View Controller is detecting the RouteMatch, I am receiving the selected line on Master View and I bind it to the View Detail. Once it is done, I am checking if the view was generated and then, calling a method to bind the Expanded entity to the iconTabBar Element which contains the 3 Fragments. The code is as follows:
function _onRoutePatternMatched(event) {
if (event.getParameter("name") === "detail") {
var path = event.getParameter("arguments").contextPath;
if (path !== " ") {
var path2 = "/" + path;
view.bindElement(path2);
if (view) {
this._setBindingToIconTab("Master2Detail");
}
} else {
view.unbindElement();
}
}
}
The _setBindingToIconTab function is as follows:
_setBindingToIconTab: function (sAssociation) {
view.byId("iconTabBar").bindElement(sAssociation);
}
iconTabBar is the ID I have assigned within the Detail view to the IconTabBar Element.
The problem is, when I execute it and it loads the first Fragment, all is ok. With the 2nd and 3rd, I got errors (but the values are displayed...). I am loading the Fragments when they are picked on the screen. If they were not generated, I instantiate and store them in an array. The errors I got are:
Assertion failed: The EDM property "DateFrom" was not found in the "ZZODATA_TEST_SRV.Master" entity type. -
sap.ui.comp.smartfield.ODataControlFactory
It is complaining about Fields from DetailEntitySet are not in MasterEntitySet.
Could you please give me a hand with this?

As there is no XML, running example etc. i can't tell you why this error occurs but..
Best practise is to biind also in the detail view the selected entity with expand to the detail and so on
MasterEntitySet->DetailEntitySet
As bindings are propagated to children there is no need to bind the iconTabBar again. Data is already there. Again i don't know your case, but most services look like this
MasterEntitySet->DetailEntitySet->DataVariantA(ForIconTab1)
->DataVariantB(ForIconTab2)
->DataVariantC(ForIconTab3)
In V4 your detail code looks like this
oView.bindObject({
path: "/MasterEntity(" + this._args.ID + ")",
parameters: {
$expand:`DetailEntityNavPath($expand=DataVariantANavPath()...`
},
events: {
dataReceived: (oEvent) => {...

Related

How can customData can be binded with JavaScript

In the affected application is a responsive table whose ColumnListItems are added via JavaScript code. Now the lines should be highlighted by the highlighting mechanism depending on their state. The first idea was to control the whole thing via a normal controller function. I quickly discarded the idea, since the formatter is intended for such cases. So I created the appropriate Formatter function and referenced it in the JavaScript code. The call seems to work without errors, because the "console.log" is triggered in each case. Also the transfer of fixed values is possible without problems. However, the values I would have to transfer are located within customData of each line...
No matter how I try to form the path I get an "undefined" or "null" output.
I have already tried the following paths:
"/edited"
"/customData/edited"
"mAggregations/customData/0/mProperties/value"
"/mAggregations/items/0/mAggregations/customData/0/mProperties/value"
The code from Controller.js (with consciously differently indicated paths):
var colListItem = new sap.m.ColumnListItem({
highlight: {
parts: [{
path: "/mAggregations/items/0/mAggregations/customData/0/mProperties/value"
}, {
path: "/edited"
}],
formatter: Formatter.setIndication
},
cells: [oItems]
});
// first parameter to pass while runtime to the formatter
colListItem.data("editable", false);
// second paramter for the formatter function
colListItem.data("edited", false);
oTable.addItem(colListItem);
The code from Formatter.js:
setIndication: function (bEditable, bEdited) {
var sReturn;
if (bEditable && bEdited) {
// list item is in edit mode and edited
sReturn = "Error";
} else if (bEditable || bEdited) {
// list item is in edit mode or edited
sReturn = "Success";
} else {
sReturn = "None";
}
return sReturn;
}
The goal would also be for the formatter to automatically use the value of the model in order to avoid its own implementation of a listener, etc.
I hope one of you has a good/new idea that might bring me a solution :)
Many thanks in advance!
You cannot bind against the customData. Because the customData is located in the element, it is like a property.
Thats why you defined it here on colListItem: colListItem.data("key", value)
You only can bind against a model.
So I see three solutions
Store the information in a separate local JSON model whereof you can speficy your binding path to supply the values to your formatter
Do not supply the information via a binding path to the formatter, but read a model/object/array from a global variable in the controller holding the information via this (=controller) in formatter function
Store the information in the customData of each element and access the element reference in the formatter function via this(=ColumnListItem).data().
Passing the context to the formatter similar to this formatter: [Formatter.setIndication, colListItem]
Cons of 1. and 2: you need a key for a respective lookup in the other model or object.
From what I understand I would solve it with solution 3.

AG-Grid with TreeData using Enterprise Row Model

I am trying to implement a grid with tree data using ag-grid. I am using the Enterprise Row Model. The problem is that when hard coding the data and setting it through setRowData the grid displays perfectly. However, when data is loaded through the enterprise row model, the grid does not render as a tree. In fact, the getDataPath callback is not even being called.
Did anyone manage to use the tree data feature with an enterprise data source as this does not seem to be documented?
Thanks
I am assuming that by Enterprise row model, you mean Serverside row model so you expect tree structured data from server. In that case, I have been able to combine following features in Ag-grid : Infinite scrolling + Tree data + server side row model.
I have even implemented custom filtering and it's working as expected.
Data flow:
We have to enable the server side row model on ag-grid using its configuration.
Implement a fake server and proxy data source objects in JavaScript. The object of data source must contain a method named "getRows" that ag-grid can call. Ag-grid will call this method every time user performs actions such as: scroll, filter, sorting, expanding a parent row to see child rows etc.
Implement a method called onGridReady() which will be called by ag-grid every time it's trying to render the grid first time, and then pass the server and dataSource objects to ag-grid's internal API inside onGridReady().
Implementation (using combination of ReactJS and plain JavaScript):
Enable serverSide row model in ag-grid.
<AgGridReact
columnDefs={this.columnDefs}
rowModelType={this.rowModelType}
treeData={true}
isServerSideGroup={this.isServerSideGroup}
getServerSideGroupKey={this.getServerSideGroupKey}
onGridReady={this.onGridReady}
cacheBlockSize={50}
/>
Implement a fake server and proxy data source objects in JavaScript.
To work with server side row model, you need to supply the data in an instance of ServerSideDataSource in JavaScript. Instance of ServerSideDataSource must have a method called getRows() which will be called by ag-grid every time user scrolls down to get next set of data or a row is expanded for retrieving its children records in Tree structure.
The constructor for ServerSideDataSource accepts a proxy data container, typcailly used in ag-grid example: a fakeServer instance. A singleton instance of fakeServer holds the data that was received from the real server and keeps it for following usage:
a) when ag-grid wants to display child records, it calls getRows. Because we supply a custom implementation of this ServerSideDataSource, we can write logic inside getRows to extract data from this fakeServer instance.
b) When ag-grid tries to display next set of data in infinite scrolling or paging, it checks how much data was last retrieved to ask for next block in infinite scrolling (using startRow and endRow variable).
Defining fake server and server side data source:
function createFakeServer(fakeServerData) {
function FakeServer(allData) {
this.data = allData;
}
FakeServer.prototype.getData = function(request) {
function extractRowsFromData(groupKeys, data) {
if (groupKeys.length === 0) {
return data; //child records are returned from here.
}
var key = groupKeys[0];
for (var i = 0; i < data.length; i++) {
if (data[i].employeeId === key) {
return extractRowsFromData(groupKeys.slice(1), data[i].children.slice());
}
}
}
return extractRowsFromData(request.groupKeys, this.data);
};
return new FakeServer(fakeServerData);
}
function createServerSideDatasource(fakeServer) {
function ServerSideDatasource(fakeServer) {
this.fakeServer = fakeServer;
}
ServerSideDatasource.prototype.getRows = function(params) {
console.log("ServerSideDatasource.getRows: params = ", params);
var rows = this.fakeServer.getData(params.request);
setTimeout(function() {
params.successCallback(rows, rows.length);
}, 200);
};
return new ServerSideDatasource(fakeServer);
}
Implement onGridReady()
Once this dataSource is ready, you have to supply this to ag-grid by calling its API method: params.api.setServerSideDataSource(). This API is available inside onGridReady() method that must be passed to Ag-grid as well. This method is mandatorily required if you're using serverSide row model.
onGridReady = params => {
...
var fakeServer = createFakeServer(jsonDataFromServer);
var dataSource = createServerSideDatasource(fakeServer);
params.api.setServerSideDatasource(dataSource);
}
Providing a key property that help ag-grid identify parent-child relationship. You have to supply these parameters to grid. Check the point (1) in Implementation that has HTML syntax and shows how to supply these methods to ag-grid.
var rowModelType = "serverSide";
var isServerSideGroup = function (dataItem) {
return !!dataItem.children;
};
var getServerSideGroupKey = function (dataItem) {
return dataItem.employeeId;
};
Notice that in getServerSideGroup(), we are returning a boolean value which checks whether children property of current row (i.e. dataItem) has any children or not.
I would request you to separately look through documents for server side row model for each feature and that means Tree data (client model) and Tree data (server side model) have two different approaches. We can't setup one model and expect it to work with data of other model.
Documentation for server side row model : https://www.ag-grid.com/javascript-grid-server-side-model/
Please try this and let me know. I had these requirements a month ago, so I contacted them for their help on Trial Support and they have prepared a plunker for this problem statement:
Working example for Tree data from server side with infinite scroll. https://next.plnkr.co/edit/XON5qvh93CpURbOJ?preview
Note:
Since the tree data is being retrieved from server, you can't use getDataPath.
Tree data would be in nested hierarchy per row unlike the client-side tree model. So unique column names are not encapsulated in an array.
Wrong :
var rowData = [
{orgHierarchy: ['Erica'], jobTitle: "CEO", employmentType: "Permanent"},
{orgHierarchy: ['Erica', 'Malcolm'], jobTitle: "VP", employmentType: "Permanent"}
...
]
Right :
[{
"employeeId": 101,
...
"children": [
{
"employeeId": 102,
...
"children": [
{
"employeeId": 103,
...
},
{
"employeeId": 104,
...
}
]},
]}
}]
There was one point when the grid was not being rendered at all in initial phase when I was just setting up the grid with Enterprise features and so it's their recommendation to use px instead of % for height and width of the wrapper DIV element that contains your Ag-grid element.
Edit:
If you prefer to fetch children record in separate API calls to save initial load time then you can make these API calls inside getRows() method. API call will have success and error callbacks. Inside the success callback method, once you receive the children data, you can pass them to ag-grid using:
params.successCallback(childrenData, childrenData.length);
Example: When a parent row is expanded, it sends a unique key of that parent row (which you must have configured already) through params.request.groupKeys. In this example, I am using JavaScript's fetch() to represent API calling. This method accepts ApiUrl, and optional request parameters' object in case of POST/PUT requests.
ServerSideDatasource.prototype.getRows = function(params) {
//get children data based on unique value of parent row from groupKeys
if(params.request.groupKeys.length > 0) {
fetch(API_URL, {...<required parameters>...})
.then(response => response.json(), error => console.log(error))
.then((childrenData) => {
params.successCallback(childrenData, childrenData.length);
});
}
else {
//this blocks means - get the parent data as usual.
params.successCallback(this.fakeServer.data, this.fakeServer.data.length);
}
};
Adding import 'ag-grid-enterprise' followed by initializing the enterprise key resolved the issue of getDataPath not working correctly.
Using example provided by Akshay Raut above, I was inspired to combine server side pagination, and client side grouping. Following his steps, with a slight change. On the getServerSideDatasource function, you can check if the parent node have the children, and rather than calling the server, just return the children directly. That way, you will be able to display the already loaded children as client side. Here is a sample code:
getServerSideDatasource(): IServerSideDatasource {
return {
getRows: (params) => {
if (params.request.groupKeys.length > 0) {
params.success({
rowData: params.parentNode.data.sales,
rowCount: params.parentNode.data.sales.length,
});
} else {
// Your regular server code to get next page
}
Here is a stackblitz:
https://stackblitz.com/edit/ag-grid-angular-hello-world-1gs4jx?file=src%2Fapp%2Fapp.component.ts
Make sure you are using gridOptions.treeData = true An example is here.
Hierarchy of data should be properly set when you implement the gridOptions.getDataPath(data)
Make sure you have implemented Enterperise.getRows Read more
If the above things don't work, share your code here to understand the overall picture better.
Infinite Scrolling or Enterprise/Serverside datasources are not compatible with Tree Data
https://www.ag-grid.com/javascript-grid-row-models/
So you have to either change your code to use Client Side Row Model or use Row Grouping (only available in enterprise)

MVC how do i populate dropdownlist without using a model

I am using mvc 4, i have a form in a view (not binded to a model), its using standard html elements.
I want to populate a dropdownlist from a list value (i.e return from controller action)
also based on the selection of a value from the first dropdownlist i want to populate a second dropdownlist
can someone please guide
for the first dropdownlist you loop thru all the available options and add in <option> tags inside <select> for the second dropdownlist you need to either make bunch drop downs and hide/show them, or you create one big list and remove invalid entries base on the first list's selection. You would definitely need to do javascript for 2nd list.
If you don't want to use a Model (you should though), you will have to add the items to ViewData
I'll stub something out for you and you can complete the rest.
Inside your controller, create a list object of what you need. If you are using EntityFrameWork this will look familiar.
var list = context.Table.ToList();
List<System.Web.Mvc.SelectListItem> ddlItems = new List<System.Web.Mvc.SelectListItem>();
foreach (var item in list)
{
ddlItems.Add(new SelectListItem(){ Text = item.Text, Value = item.Value.ToString()});
}
ViewData["DDLItems"] = ddlItems;
#Html.DropDownListFor(x => x.LeagueId,
new SelectList((System.Collections.IEnumerable)ViewData["DDLItems"], "Value", "Text")
, "--Select League--", new { id = "league" })
You can define your second dropdownlist with just a placeholder until the cascade effect happens.
#Html.DropDownListFor(x => x.DivisionId, Enumerable.Empty<SelectListItem>(),
"--Select Division--", new { id = "ddlDivision" })
Your going to need to use JQuery and fire and event when the dropdown changes, and then use Ajax to make a call back to the controller. Theres 2348239 examples online about making Ajax calls, know how to do that because it's done all the time in MVC.
I'll let you figure that part out. One hint, inside the Ajax call you can pass data to the controller. something like this data: { leagueId: value } where value is the value of the dropdownlist you want to cascade off of. leagueId must match type and name of the parameter your controller will expect.
Return a Json object from your controller like so...
public JsonResult GetDivisions(int leagueId)
{
var division = //similar to before, fill a list.
return Json(divisions, JsonRequestBehavior.AllowGet);
}
And then in the success function of your Ajax call, you will populate the Second dropdownlist.
success: function (data) {
$.each(data, function (index, item)
$('#ddlDivision')
.append($('<option></option>')
.val(item.Value)
.html(item.Text))
item.Value and item.Text can be anything, just as long as the Json you return as the properties of Text and Value
IE...
var divisions = (from x in context.Division
select new
{
Text = league + " " + x.Region,
Value = x.DivisionId
}).ToList();

SharePoint - Set custom publishing page layout custom field programatically

I have a custom publishing page content type, based on the Publishing Article Page content type. On this content type, I have a custom field named "PageContentCategory". In my code to create new pages, I tried this:
PublishingPage newPublishingPage = this.currentPublishingWeb.GetPublishingPages().Add(pageName, newPageSelectedLayout);
if (pageContent.IsEmpty())
{
pageContent = Properties.Resources.EAWorldArticleHandler_CreateNewArticlePage_DefaultPageContent;
}
newPublishingPage.ListItem[new Guid("{93496B35-7EC3-4132-B0D0-3BDC5606F5EF}")] = pageContentCategory;
newPublishingPage.ListItem[FieldId.PublishingPageContent] = pageContent;
newPublishingPage.Title = pageTitle;
newPublishingPage.Update();
I have also tried to set it by the field name:
PublishingPage newPublishingPage = this.currentPublishingWeb.GetPublishingPages().Add(pageName, newPageSelectedLayout);
if (pageContent.IsEmpty())
{
pageContent = Properties.Resources.EAWorldArticleHandler_CreateNewArticlePage_DefaultPageContent;
}
newPublishingPage.ListItem["PageContentCategory"] = pageContentCategory;
newPublishingPage.ListItem[FieldId.PublishingPageContent] = pageContent;
newPublishingPage.Title = pageTitle;
newPublishingPage.Update();
Both of these methods throw an error. Is there any way for me to set my custom field's value in code like this?
Try calling the Update method on newPublishingPage.Listitem not on newPublishingPage itself.
Like this:
newPublishingPage.ListItem["PageContentCategory"] = pageContentCategory;
newPublishingPage.ListItem.Update();
and then you maybe also need some of these lines, depending in the configuration of your page library
newPublishingPage.Checkin();
newPublishingPage.Publish();
newPublishingPage.Approve();
So, the solution to my problem was that I had to programmatically add the content type to the pages list instead of letting it be added automatically the first time a page with that content type was added. Apparently if you let SharePoint automatically add the content type to the pages list then it somehow doesn't get bound properly. So adding the content type first solved my problem.

MVC Areas and routing

I'd like to have an area called "Products", where I can use routes such as
http://localhost/products/foo
http://localhost/products/bar
I would like to have the views and other assets organized into a folder structure like
/areas/products/views/foo/index.aspx
/areas/products/views/bar/index.aspx
I'd like to keep images, etc specifically related to each product (foo, bar) in their respective /area/products/views/(foo|bar)/ folder.
I also do not want to have to add a controller action for each product.
If I declare a route like
context.MapRoute(
"products-show-product"
, "Products/{id}"
, new { controller = "Products", action = "Index", id=UrlParameter.Optional }
);
and request the url
http://localhost/products/foo
then ProductsController.Index() is called, as I would expect. However, since the view "foo" is not in the views/products or views/shared folder, it isn't being found.
How can I do this so that I can keep each product's pages in a separate folder?
I don't have a concrete answer to your question since I am not sure about my understanding of it. However I have a general feeling for the direction for the solution.
When one starts to change locations of views, the corresponding methods that find those views also need to change. A simple approach would be to override the FindView and FindPartialView methods.
A simple demo. I created an Area called Blog, a Blog controller with an Index method. In my case I user the controller action as the SubFolder but I am sure that this can be extended to your case for each product folder. I assume that the product will be a request argument. Area http://www.freeimagehosting.net/uploads/85b5306402.gif
The basic idea is to interrogate the controllercontext for the controller, area, action and id and modify the what the default viewengine looks for. The default locations for area views looks like "~/Areas/{2}/Views/{1}/{0}.aspx", so we can basically inject values for the view name and in this case ActionName/Index. The view location will end up being ~/Area/Blog/Views/Blog/Index/Index.aspx.
This is just a rough outline, of the code that can be used. The string comparisons can definitely be updated to more robust methods. As it stands this method will work for the entire app as expected, except for the case when a request is made to the Blog area for the Index action.
public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)
{
if (controllerContext.RouteData.DataTokens ["Area"] == "Blog" )
{
if (String.Compare(controllerContext.RouteData.Values ["Action"].ToString(),"Index",true) == 0)
{
var viewLocation = String.Format("{0}/{1}", controllerContext.RouteData.Values["Action"].ToString(), viewName);
return base.FindView(controllerContext, viewLocation , masterName, useCache);
}
}
return base.FindView(controllerContext, viewName, masterName, useCache);
}