logo

Easy Access to Sub-Collections of the DocumentWrapper Class

My favourite feature of the Wrapper Classes I keep going on about is the ready access they can give to any given class's related sub-classes.

For example, let's say you have an object based on the Customer class and you want to find all their Invoices. Wouldn't it be nice to write code like this in a WQO agent:

Dim customer As Customer, invoice As Invoice
Set customer = New Customer(web.document)

If customer.Invoices.Count > 0 Then
  Set invoice = customer.Invoices.getFirst()
  
  While Not invoice Is Nothing 
    'Do what you like here
    
    Set invoice = customer.Invoices.getNext()
  Wend
End If

When you start using code like this in your day-to-day LotusScripting that's when it all starts to gel and there's no going back.

Imagine also that you wanted to know how many other invoices the customer for any given invoice has. You can do it like this:

Print "This invoice's customer has " + Cstr(invoice.Customer.Invoices.Count - 1) + " other invoices"

Cool, no?

Adding Collection Properties

But how do we add this "Invoices" property? Easy. Just do something like this in the Invoice class:

Class Customer as DocumentWrapper
 Private Invoices_ As InvoiceCollection
 Private Factory_ As InvoiceFactory

 Property Get Invoices As InvoiceCollection
  'lazy load the invoices!
  If Invoices_ Is Nothing Then
   Set Factory_ = new InvoiceFactory()
   Set Invoices_ = Factory_.GetInvoicesForCustomer(Me)
  End If

  Invoices = Invoices_
 End Property
End Class

Noticed we've defined a class-level InvoiceCollection and InvoiceFactory (we need to do this so they stay in scope). We then "lazy load" them the InvoiceCollection when it's first requested.

Here we're only going one level of sub-class deep. But you could go as many levels deep as you needed.

In the example above I've called a method called GetInvoicesForCustomer(). How this works depends on how your data is structured. Maybe the invoices are child documents of the Customer. Maybe they're just tied together by an "id" field. Either way, it doesn't really matter.

Comment Icon There are 4 comments in 3 threads Read - Add

Creating New Documents Based on the DocumentWrapper Class

So far, we've seen how to "wrap" an existing document inside a custom class, which is based on the DocumentWrapper subclass. But the document we wrap doesn't have to exist. We can use the DocumentWrapper classes to handle creation of new documents.

Imagine that, anywhere in your LotusScript, you could write this:

Set invoice = New Invoice(web.database.CreateDocument)
invoice.Number = factory.GetNextInvoiceNumber()
invoice.Value = 12345.98
invoice.Save()

Wouldn't that be cool!?

It's quite simple to do. To help take some of the pain out creating new documents of a certain type you can use the "New()" method of the derived class to check if it's a new document being created. If it is new then we can "initialise" it by adding some of the key fields and values. Such as the "Form" field.

So, for the Invoice class the New() method would look like this:

Class Invoice As DocumentWrapper
 Sub New(doc As NotesDocument), DocumentWrapper(doc) 
    If doc.IsNewNote Then 
         SetFieldValue "Form", "Invoice" 
         AllowedEditors = web.user.Canonical
         AllowedReaders = Split("*/ACME;[Administrators]", ";")
        'Any other fields this form *needs* to have!?
    End If 
 End Sub

 Property Set Value As Variant
  SetFieldValue "Value", CCur(Value)
 End Property

 Property Set Number As String
  SetFieldValue "Number", Number
 End Property

End Class

So far the derived classes I've shown only had "getter" properties, which returned field values. But the above two snippets of code demonstrate the user of "setter" properties also.

Notice the AllowedEditors and AllowedReaders properties. These are a new addition to the base DocumentWrapper class, which we can use to add Names-type fields. In turn they rely on a new AddNamesField method, like so:

Property Set AllowedEditors As Variant
 AddNamesField "DocAuthors", AllowedEditors, AUTHORS 
End Property
        
Property Set AllowedReaders As Variant
 AddNamesField "DocReaders", AllowedReaders, READERS
End Property


Sub
AddNamesField(FieldName As String, UserNames As Variant, FieldType As Integer) Dim item As New NotesItem(document_, FieldName, UserNames, FieldType) End Sub

All document creation logic for each business class/model can now be encapsulated in one place. Then, no matter where in your code you find yourself creating new instances from, you don't need to worry about what the form name is or what fields are needed.

As I keep saying: the more I use and extend these three classes the more I wonder how I got by without them...

Comment Icon There is 1 comment Read - Add

Extending The LotusScript Wrapper Classes

The wrapper classes I blogged about in February went down a lot better than I thought they might.

So, taking it further. Let's extend the DocumentWrapper class some more. Since first writing about them  on here I've been busy using and extending them as I go. Here are a few examples of some of the properties I've added

Simple Helper Properties

Some very simple properties: WebLink, UNID and IsNew.

Property Get WebLink As String
  WebLink = "/" + Replace(web.database.Filepath, "\", "/") + "/0/" + UNID + "?OpenDocument"
End Property
        
Property Get UNID As String
  UNID = Document.Universalid
End Property
        
Property Get IsNew As Boolean
  IsNew = document.Isnewnote
End Property

These don't do anything that can't already be done. They just make it simpler and quicker to code.

Logging Document Actions

Properties are all well and good. But they're just borne of laziness really. Let's add a method or two, to make it really useful. How about adding a new method to the base class called LogAction() which you might use like this:

invoice.LogAction("Invoice Approved")

And the method might look like this (it assumes all your Forms have a standard field called "ActionLog"):

Sub LogAction( Action As String )
  Dim item as NotesItem
  Set item = document.GetFirstItem( "ActionLog" )
  Call item.AppendToTextList( Action + " on " + Format(Now, "dd mmm yy") +_
   " at " + Format(Now, "hh:nn:ss") + " by " + web.User.Abbreviated )
End Sub

The web.user part assumes you're already using my WebSession Class.

Deleting Documents

If you never actually delete documents, but merely flag them as deleted then you can give all objects based on the DocumentWrapper class a Delete() method, like so:

Sub MarkDeleted()
 Call SetFieldValue("Delete", "1")
 Call LogAction("Document Deleted")
End Sub

You can then add an IsDeleted property to the DocumentWrapper class, like so:

Property IsDeleted As Boolean
  IsDeleted = (GetFieldValue("Deleted")="1")
End If

At this point you might consider moving some of the field name and their values to "static strings" at the top of the class, then changes the corresponding methods, like this:

Const DELETED_FIELD_NAME = "Deleted"
Const DELETED_FIELD_VALUE = "1"

Sub MarkDeleted()
  Call SetFieldValue(DELETED_FIELD_NAME, DELETED_FIELD_VALUE)
End Sub

Property IsDeleted As Boolean
  IsDeleted = (GetFieldValue(DELETED_FIELD_NAME) = DELETED_FIELD_VALUE)
End If

Naturally you might want to add an UnDelete() method too...

Extending Derived Classes

So far I've talked about extending the base DocumentWrapper class. Everything we add there is available to all classes derived from it. But, obviously, you can also add properties and methods to the derived classes. For example, the Invoice class might want an Approve() method. This could either simply flag it as approved or perform some more complex workflow logic, passing it between approvers.

Class Invoice As DocumentWrapper
 Sub Approve()
  'Do whatever 
  LogAction("Approved")
 End Sub
 
 Sub AddItem(Description as String, Value as Currency)
  'Add to the relevant fields
 End Sub
End Class

Notice I added a call to LogAction() inside the Approve() method. That way you never forget to log the approval no matter where you call the Approve() method from.

The possibilities are almost endless.

Summary

These wrapper classes have completely changed the way I code my Domino web apps! My dream has always been to be a "proper coder". I'd love to be able to work completely in Visual Studio writing C#. Unless you've done that you won't appreciate what a pleasure it can be. I get a buzz from writing my own classes and then using them. This is a buzz I've never really had while coding for Domino. Until now. These wrapper classes will have to do for the time being.

Next week I'll talk even more about them and show how to handle collections of documents that belong to other documents. While I go, I'm working on a demo app that brings it all together.

Comment Icon There are 15 comments in 5 threads Read - Add

How to Do Infinite Scrolling in Domino

Last week I linked to a Domino-based infinite-scrolling view demo and promised I'd write up how I did it. Even though the reaction to the demo was mixed I'm a man of my word, so here goes.

It always surprises (worries!) me when I'm asked to either share a downloadable copy of a demo or write a how-to about it. Particularly when it's something that's all but completely web-based. The code is there to see; just a click away.

That said, in this case, unless you're familiar with jQuery and/or jQuery plugins it might not make much sense and warrants some explaining, so here goes.

First bit of code you see is at the very bottom of the scrolling view page and looks like this:

//Setup the "infinite" scrolling view
$(document).ready(function(){
    $('#invoices').infiniteScroller({
        URL: "(getInvoices)?OpenAgent",
        viewName: "InvoicesByDate",
        emptyArraySize:1,
        pageSize: 40,
        itemTemplate: "<tr><td><%=Document.Created.Formatted%><td><%=Title%></td><td><%=Customer.FullNameReversed%></td><td style=\"text-align: right;\"><%=Value.Formatted%></td>"+
        "<td><span class=\"label <%=Status.LabelClass%>\"><%=Status.Description%></span></td>"+
        "<td><a class=\"btn btn-link\" href=\"0/<%=Document.UNID%>?Open\">View &raquo;</a></td></tr>",
        endTemplate: "<tr><td colspan=\"6\" style=\"text-align:center\"><strong>There are no more invoices to load!</strong></td></tr>"
    });
});

You may recognise the .ready() bit as being jQuery's way of having the enclosed code run as soon as the document is done loading/rendered.

But what about that $().infiniteScroller() bit!? Well, that's a call to the jQuery plugin part that I wrote. That might sound difficult, but it's not.

In essence you can define a jQuery plugin, which I've done in the "Application.js" file, like this:

(function ($) {
    var Scroller = function (element, options) {
        this.$element = $(element);
        this.options  = options;

        //Do what you like here.
        //Remember that this.$element always points to
        //the HTML element we "invoked" infiniteScroller on!

    }

    $.fn.infiniteScroller = function (options) {
            return new Scroller(this, options);
    };

})(window.jQuery);

Notice the part in bold! This is where we extend jQuery's $() selector with our own method name. Simple, but so powerful.

Obviously I've missed out most of the actual code above. All that the missing code does is call an Agent via Ajax and append the resulting data as HTML <tr> elements on to this.$element.

The only tricky part is handling the auto-scrolling. To do this we bind an event handler to the windows onScroll event when the page has loaded, like so:

$(window).bind('scroll', {scroller: this}, this.scrollHandler);

This adds the Scroller's scrollHandler method as a listener and passes a reference to the current Scroller in to the event's data. Which we can use in the scrollHandler method, like so:

Scroller.prototype.scrollHandler =  function(event){
    var $this = event.data.scroller; 
        
    //Are we at (or as near) the bottom?
    if( $(window).scrollTop() + $(window).height() > $(document).height() - $this.options.pixelBuffer ) {
        $this.currentPage++;
        $this.loadData();
    }  
};

Fairly simple, no? It just tests to see if you're at the bottom of the page. Using the "pixelBuffer" option you can make it so they don't need to be right at the bottom, if, for reason, scrolling needs to happen before they get to the bottom. You could even pre-empt them getting there by loading when they're getting close to the bottom and they'd never need to wait.

What About the Backend?

Back on the server things are actually quite simple. Perhaps you thought that wasn't the case?

To get it to work the first thing I did was extend the DocumentCollection wrapper class I talked about a few weeks back by adding a getNth() method to it.

Now, all my Agent needs to do is this:

Dim factory As New InvoiceFactory Dim invoices As InvoiceCollection Dim invoice As Invoice Dim start As Long, count As Integer, i As Integer Dim View As String start = CLng(web.query("start", "1")) count = CInt(web.query("count", "40")) view = web.query("view", "InvoicesByTitle") Set invoices = factory.GetAllInvoices(view) 'Invoice to start at Set invoice = invoices.getNth(start) Print "content-type: " + JSON_CONTENT_TYPE Print "[" While Not invoice Is Nothing And i<count Print invoice.AsJSON + |,| i = i + 1 Set invoice = invoices.getNext() Wend Print |null]| 'NASTY, NASTY hack to avoid dropping last , from array. Yack!

Note that the "web" object referred to above is based on the WebSession class which has been a mainstay of my Domino development for the past 5 or 6 years.

The more I use these new "wrapper classes" in LotusScript the more I kick myself for not having thought of them earlier on in my career. Although I am now using them on a daily basis and they're saving me time I keep thinking of how much time they could have saved over the years.

Having said that, just last week, for the first time in ages, I pressed Ctrl+N in Domino Designer and started a new Domino project from scratch (who says Notes is dead!?). The wrapper classes are an integral part of this new project.

Talking of the wrapper classes. I'll be updating and talking about them shortly. I've made significant changes to them and I think you'll like.

Comment Icon There are 8 comments in 4 threads Read - Add

Infinite Scrolling Views in Domino

On Tuesday I was talking about whether it does a user an disservice to simply give them a selection of "Form X by Field Y"-type Notes views when building for the web.

I then (over-)promised to show some alternatives. Hopefully you're not expecting anything revolutionary, coz that they ain't. Just something different. Extra tools for the toolbox.

Infinite Scrolling

The first alternative is to use "infinite scrolling". This is something you'll be used to if you use Twitter or Pinterest. The pages load the most recent content and, when you scroll to the end of that list, it loads more. And so on, until there's no more to load.

The screenshot below is of this Domino-based "infinite scrolling" view.

image

Combined with the fact some of the column headers are sortable/clickable it makes the "Invoices by X" links in the nav bar redundant, as it does page navigation.

What surprised me was how "simple" it was to implement. At first I'd gone looking for ready-built jQuery plugins to do it, but they all weighed in at +20kb and were either overkill or not particularly suited to Domino (nothing ever is!).

So I wrote my own jQuery plugin to do the job, using about 20 - 30 lines of code. I'll talk more next post on the code involved. For now I want to discuss the pros and cons of these types of view.

Advantages

Hmm. What are the advantages? I'm not really sure. They're nice, I guess. Saves the user having to press the "Next Page" link each time they get to the bottom. Other than that...

Disadvantages

Oddly, I can think of plenty of these, even though I'm suggesting they're use as an alternative to the normal way of doing things.

  • What I find most annoying about these views is that the scrollbar jumps out of each if, like me, you use the mouse to click the scrollbar and pull it down to scroll. Once more content loads the scrollbar puller moves and you have to move your mouse back up to grab hold of it again.
  • Once the first page loads you can't use the scrollbar's size as a visual indicator to work out how much content there actually is.
  • It breaks the back button (although there are ways round that)
  • It blocks access to the page's footer area, as you can never quite get to it until all the content has loaded! In the example above, there isn't anything in the footer, but if you do have something like FAQ or Contact Us links you don't want to prevent people ever being able to scroll down to them!

As with everything and as I always say, it's a case of "horses for courses". Hopefully when I show the code in the next post you'll see these auto-scrolling views aren't as hard to implement as you might have thought.

Comment Icon There are 13 comments in 8 threads Read - Add

‹ Previous | « Firsticon Page 5 of 344Last » | Next ›