I am trying to understand the dataContext better and tried creating a dataContext referencing a document.
<xp:this.dataContexts>
<xp:dataContext var="doc1">
<xp:this.value>
<![CDATA[#{javascript:
var db:NotesDatabase = sessionAsSigner.getDatabase("","privDb.nsf");
var adoc:NotesDocument = db.createDocument();
return adoc }]]>
</xp:this.value>
</xp:dataContext>
</xp:this.dataContexts>
I then tried using EL and javascript to bind fields on my xpage to the dataContext
<xp:inputText id="inputText2" value="#{doc1.lastname}"></xp:inputText>
<xp:inputText id="inputText1" value="${javascript:doc1.firstname}"></xp:inputText>
But when I save, it does't save anything.
<xp:button value="save" id="button1">
<xp:eventHandler event="onclick" submit="true"
refreshMode="complete">
<xp:this.action><![CDATA[#{javascript:
print(doc1.getClass().getName() )
doc1.save();
}]]></xp:this.action>
</xp:eventHandler>
</xp:button>
The print command showing me the class name is showing up as lotus.domino.local.Document
The document is saved to the database, but it has no values other than $UpdatedBy. I can't seem to bind fields to the edit boxes.
The reason I am going down this path is twofold, 1. I want to use sessionAsSigner so I can keep the database security on the remote db (privDb.nsf) at No Access for anonymous and default, and 2. I want to learn a little more about dataContext, data sources and binding. I have read the "easy" way of using a Public Document, using the $PublicAccess field, etc, which is the "Old School" Notes way, and yes, I could do it that way, but want to understand how to do it using dataContexts, if possible.
A dataContext is basically a scoped variable, scoped lower than viewScope but higher than requestScope, scoped to a component. That component can be an XPage, a Custom Control or a Panel (yes, dataContexts can be added to a Panel too).
Like other scoped variables on the page, a simple save action does not save dataContexts. If you want a variable that's create-able and save-able, that's the Data Object. That has specific createObject and saveObject properties, where you define what should happen when they're called by the XPages runtime.
Similarly, like the other scoped variables, it needs to be serialized, so you cannot store a Domino object in them. So you can't store a NotesDocument in them. You need to wrap a normal Java object around a NotesDocument. With a greater understanding of XPages it becomes apparent that's what the dominoDocument datasource is doing (creating properties for all fields on the document, storing its note id, UNID, adding other properties like whether it's in edit mode or new etc).
A final point, as Jesse says, dataContexts are re-evaluated multiple times during a partial refresh. I haven't re-tested recently, but under 8.5.3 dataContexts bound to an XPage or Custom Control were re-evaluated more than dataContexts bound to a Panel, so I'd recommend the latter.
The immediate problem that you're running into is that #{}-bound dataContexts are re-evaluated constantly, several times during a page load and, I believe, every time they're referenced. Generally, the rule of thumb with dataContexts (and don't get me wrong - I like them) is that they should either be extremely low-cost, like a quick mathematical calculation, or be ${}-bound. The latter wouldn't work here, though, since the document wouldn't survive past the first load.
The tack you may want to try is to use a dataContext like this:
<xp:dataContext var="docData" value="${javascript: new java.util.HashMap() }" />
Basically, using a simple object as a holding pen. Then, in the save action, create the new document and set all of the values from "docData" there, like:
var db = sessionAsSigner.getDatabase("", "privDb.nsf");
var doc = db.createDocument();
doc.replaceItemValue("firstname", docData.get("firstname"));
doc.replaceItemValue("lastname", docData.get("lastname"));
doc.save();
There are a few caveats to this approach:
Each save will create a new document, rather than editing the existing one. You could change this by storing the UNID back into the map and fetching it from the DB, though
It should work as-is in this case, but you'll have to be mindful of data types. For example, if you have a multi-value control, then the XPages runtime will probably create an ArrayList, which you would have to convert to a Vector for storage unless you're using the OpenNTF Domino API
And as a final note, that binding you have for firstname is almost definitely not what you want. That may just be an artifact of the testing you were doing, but I'd be remiss if I didn't mention it.
Related
For my XPage I developed some Managed Beans to keep data. The page itself contains a Repeat control, it's like this at the moment:
<xp:panel styleClass="form">
<xp:this.dataContexts>
<xp:dataContext value="#{javascript:PageData.getForm(compositeData.formName, compositeData.dataSource);}"
var="myFormData">
</xp:dataContext>
<xp:dataContext var="myFields">
<xp:this.value><![CDATA[#{javascript:return myFormData.getFieldsAsJSON();}]]></xp:this.value>
</xp:dataContext>
</xp:this.dataContexts>
<xp:table style="width:100%" cellspacing="1" cellpadding="0">
<xp:repeat var="thisfield" rows="#{javascript:compositeData.rows}" disableTheme="true" repeatControls="true"
disableOutputTag="true" value="#{myFields}">
The PageData object is a bean, it contains info about a form and its fields. The dropdown fields contain options, but they can be dynamically created. When that happens, as a result of a partial refresh, the form is redisplayed but... the repeat 'value' itself (myFields) isn't reloaded into the repeat, so new dropdown options aren't made available.
How can I make XPages reload the 'value' property?
UPDATE
More info, as requested...
From the FormData bean:
public Object getFieldsAsJSON() {
ArrayObject list = null;
try {
System.err.print("FormData: getFieldsAsJSON");
list = new ArrayObject();
for (Entry<String, FieldData> e : fields.entrySet()) {
FieldData field = e.getValue();
list.addArrayValue(field.getJSON());
}
} catch (Exception e) {
}
return list;
}
and from the FieldData bean:
public ObjectObject getJSON() throws InterpretException {
ObjectObject json = new ObjectObject();
json.put("name", FBSUtility.wrap(name));
json.put("label", FBSUtility.wrap(field.getLabel()));
json.put("options", FBSUtility.wrap(values));
return json;
}
IMHO the problem isn't in these beans. It's more an XPages issue, because for some strange reason the structure is only fetched once, and never again. Apparently, XPages considers the json to be static, which it isn't.
Before the bean-era, I had everything in SSJS, creating the JavaScript object on the fly, and it was reloaded every time. Why not anymore, what is the difference? Is there something to tell XPages that the repeat-value is 'stale' and should be re-read?
With your current code myFormData and myFields will be recalculated during every partial refresh as well. Is that necessary?
I'm not certain, but looking at what you're seeing, I suspect the repeat control needs the contents of the dataContexts at a particular phase of the XPages lifecycle. But the dataContexts will be recalculated at every phase of the XPages lifecycle, which may result in them being null when the repeat wants to use the values.
My best advice is to debug what's happening at which phases in the partial refresh. You'll probably need to change your code to work around what's calculated when.
I'm not convinced dataContexts set to be computed dynamically are a good approach unless you're using them for rendered properties or read-only data. If you're using a managed bean, lazy-loading the data and retrieving it directly from the bean may be a better approach, as well as making it easier to identify when it's being called.
I have a simple button with the eventHandler:
<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
<xp:this.action>
<xp:openPage target="newDocument" name="/doc.xsp"></xp:openPage>
</xp:this.action>
</xp:eventHandler>
In lotusScript I would add a value to a field, before composing the form, using:
Call doc_new.ReplaceItemValue("txt_codformularmain","01")
....
....
Set uidoc = w.EditDocument ( True, doc_new )
I tried in the postNewDocument event of the doc.xsp
<xp:this.data>
<xp:dominoDocument var="Contr" formName="(fmFormularCIP)">
<xp:this.postNewDocument><![CDATA[#{javascript:Contr.replaceItemValue("txt_codformularmain", "01")}]]></xp:this.postNewDocument></xp:dominoDocument>
</xp:this.data>
I don't want every time the doc is created that value to be, let say, 01, BUT only when the doc. is composed by a specific button. Some other button will have the chance to add the 02 value for that field, and so on.
How can I achieve this in xpages development? Thanks for your time.
This is a little bit tricky in Web application development, because what you did in classical Notes development cannot be applied here. I had a lot of issues in this.
The classical scenario is that you have a Page X with some value (say txt_codformularmain will be 01). How do you decide this value "01"?
In some cases, this value is something you have in the page X. So you want to pass a specific value to the target page. One option could be using a query parameter (doc.xsp?myValue=01) and consume it on doc.xsp with param.myValue. Or, you might put a sessionScope variable before going to the target page and consume it by sessionScope.myValue.
It depends on what you need. Query parameter is somewhat safer, because the user might use the same button twice and sessionScope variable causes inconsistent behaviour. On the other hand, query parameter can be changed by the user, so you might have a breach in your application.
Some cases, you want to populate some values at the second page. Simple values (e.g. creation date, user names, etc.) can be populated with forms (i.e. compute value on load). Sometimes it would be more complicated (e.g. the department of the user where you have to lookup some views). For such cases, you need to use postNewDocument event at the target page.
After you have passed the value to the doc.xsp page, you will consume in the way you need. If it's not going to be edited on the page, you may again use postNewDocument event and set value by dataSourceName.replaceItemValue('itemName', param.myValue). If the value can be edited, you would use it as a default value on your field component.
One problem I have experienced: Once you set a value in postNewDocument, you may have problems to change it later, until the document data source saved. This is happening on specific cases. If you experience such a problem, you might use some editable field with readonly attribute. Just keep in mind :)
I realized my question was too vague on adding navigation items dynamically, so I am rewriting the question.
I have discovered the xe:dominoViewEntriesTreeNode control from the xpages. I think I can use this to add navigation items to the navigator control based on entries in the view.
I am struggling to find very much in the way of documentation or resources that break down how to do that. Can anybody to me to a good reference or example code?
You can use dominoViewListTreeNode to build a menu based on views in a database (and not documents in those views).
Here is an example of using xe:dominoViewListTreeNode to dynamically build a menu based on all views called "Test*" (using regex in the filter property). When selecting a menu item from the menu, the name of the view is submitted to the server (using EL notation for the viewEntry.getName() method).
The example also contains an onItemClick event handler that "catches" the name of the view as the submitted value and stores this in a sessionScope variable. The event handler then redirects to a views.xsp XPage that could contain a Dynamic View Panel control where you could use the sessionScope variable to control what view to show.
The sessionScope variable is also used to mark the selected menu item as "selected".
<xe:navigator id="navigator1">
<xe:this.treeNodes>
<xe:dominoViewListTreeNode filter="Test.*" submitValue="#{viewEntry.name}" var="viewEntry">
<xe:this.selected><![CDATA[#{javascript:viewEntry.getName() == sessionScope.clickedView}]]></xe:this.selected>
</xe:dominoViewListTreeNode>
</xe:this.treeNodes>
<xp:eventHandler event="onItemClick" submit="true" refreshMode="complete">
<xp:this.action>
<![CDATA[#{javascript:sessionScope.clickedView = context.getSubmittedValue();
context.redirectToPage("views.xsp");}]]>
</xp:this.action>
</xp:eventHandler>
</xe:navigator>
Instead of the onItemClick method to redirect to an XPage, you could compute the href property of xe:dominoViewListTreeNode to return the name of an XPage.
I have a short presentation called "XPages Extension Library - Create an app in 1 hour (almost)" that presents this technique (and other techniques).
I assume you have an area on your page carrying the navigation items, e.g. links to some pages with link texts?
I would then use a repeat control with a datasource/javascript source that returns the document item values from your profile document or something.
If you are not into repeat controls then you should consider to read this: http://xpageswiki.com/web/youatnotes/wiki-xpages.nsf/dx/Work_with_repeat_controls
On this page there is also a sample dealing with a profile document.
By the way: using profile documents was always a crutch, so consider to youse "normal" config documents instead.
how can I add custom control on the basis of sessionScope variable. I try include page container control but no luck:
<xp:this.afterPageLoad><![CDATA[#{javascript:sessionScope.put("viewName","ccViewAll.xsp");}]]></xp:this.afterPageLoad>
<xp:text escape="true" id="computedField1">
<xp:this.value><![CDATA[#{javascript:sessionScope.get("viewName")}]]></xp:this.value>
</xp:text>
<xc:appLayout>
<xp:this.facets>
<xp:panel xp:key="facetMiddle">
<xp:include id="include1">
<xp:this.pageName><![CDATA[${javascript:sessionScope.get("viewName")}]]>
</xp:this.pageName>
</xp:include>
</xp:panel>
</xp:this.facets>
</xc:appLayout>
The above code give me error Error 404 HTTP Web Server: Item Not Found Exception. But when I hardcode the viewname that is ccViewAll.xsp instead of sessionScope.get("viewName"), its work fine.
-MAK
You can use the dynamic content control or the switchFacet control if you have the ExtLib for XPages. The Teamroom template (demo application that comes with the ExtLib) uses these in the "allDocuments" Xpage or the "allDocsAllTab" custom control, these are good examples to look at.
If you don't have the ExtLib you could use the loaded / rendered property of a custom control to decide which one gets loaded.
e.g.
<xp:panel key="MiddleColumn">
<xc:customControl1 loaded="${javascript: if(viewScope.control == "customControl1")}"></xc:customControl1>
<xc:customControl2 loaded="${javascript: if(viewScope.control == "customControl2")}"></xc:customControl2>
</xp:panel>
loaded = false means that nothing will be done for this control.
rendered = false means that the control will be created but hidden, you can change this later to show it.
use rendered if its going to change for example when a button is clicked and loaded when its decided at start up and won't change while the user is logged in.
If you are using this to show a different view in the domino database based on some other selection that I would suggest looking at the Extension Libraries 'Dynamic View Panel' control.
Using this control means you won't need to create different custom controls for each view that you want to use, just a single page with this control and point it to the correct view to display via a scope variable.
If you need to customize how each view displays you can create a viewControl bean to set additional properties based on the view that it is showing.
Something worth to mention is that if you don't use the ExtLib - If you're using Partial Refresh then you HAVE to use the rendered property, since the loaded property only can be calculated on pageLoad.
From my understanding this means that every custom control will be computed by the server and that's probably not something you want. (Added overhead, for one thing.)
/J
I have two forms, Organisation and Contact. Contact is a response to Org, and each form has an XPage where the form can be filled in, saved etc. When opening edit_contact.xsp directly and creating a document (not as a response to an org), everything works fine.
On edit_org.xsp, I have a button with 2 events. The first copies some values into sessionScope so I can inherit them into the Contact. The second is a "Create Response Document" event which creates a new response with the parent ID being the current org document, and sends the user to edit_contact.xsp. Pressing the button changes XPage correctly and the field inheritance works fine, but pressing "Submit" on the Contact form doesn't save anything, and no document is created.
This exact same setup works 100% as desired in another database, I have no idea why it won't work properly here. Is there an obscure setting somewhere I am missing?
<xp:button value="Create Contact" id="button1" rendered="#{javascript:!document1.isEditable()}">
<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
<xp:this.action>
<xp:actionGroup>
<xp:executeScript>
<xp:this.script>
<![CDATA[#{javascript:var doc = document1.getDocument();
sessionScope.PFirstName = doc.getFirstItem("P_Firstname").getValueString();
sessionScope.PSurname = doc.getFirstItem("P_Surname").getValueString();
sessionScope.PFamily = doc.getFirstItem("P_Family").getValueString();
sessionScope.PDOB = doc.getFirstItem("P_DOB")
sessionScope.PAGE = doc.getFirstItem("P_Age").getValueString();}]]
</xp:this.script>
</xp:executeScript>
<xp:createResponse name="/edit_contact.xsp" parentId="#{javascript:document1.getNoteID()}">
</xp:createResponse>
</xp:actionGroup>
</xp:this.action>
</xp:eventHandler>
</xp:button>`
Here is a link that shows what I am trying to do (minus the field inheritance):
http://min.us/mKSJED8tT
Currently the forms and views all work, but the document created with the "Response" form appears not to be a response document - it has no $REF field. This setup works perfectly in a different database - what is going on?
It's hard to tell what might be happening without seeing any code. Since it works for you in another database, could it be an ACL issue? Where the user you're logging in as - possibly - anonymous - doesn't have the ability to create documents?
Instead of "push" approach go for "pull" - just open response page with url parameter of parent document. In its postNewDocument event initialize field values from it.
It would be useful to get an update on two key points made by others:
Is ignoreRequestParams set on your response document datasource? If not, regardless of what you're trying to define for the second datasource, it's UNID etc are pulled from the request parameters. So both datasources are effectively the same datasource.
Is it throwing a validation error? If so, nothing will be saved.
There are two possible issues:
First the use of getFirstItem("x") is not a best practice. So:
sessionScope.PDOB = doc.getFirstItem("P_DOB")
would be storing a NotesItem in the sessionScope which will not work. It is recommended to use:
sessionScope.PDOB = doc.getItemValueString("P_DOB");
Second the use of getNoteID() might not be returning what you want (Which is the UNID of the document). Use .getDocument().getUniversalID() instead.
<xp:createResponse
name="/edit_contact.xsp"
parentId="#{javascript:document1.getDocument().getUniversalID()}">
</xp:createResponse>
-edited-
/Newbs