You are currently browsing the monthly archive for April 2010.

David Van Puyvelde inspired with CMS Force and Wes Nolte inspired with an article on JQuery validation in Visualforce. So I thought why not bring the package together? So I did:

This exists as a component in a standard Visualforce page:

   <c:contactForm onCompleteJavascript="$('#registerUser').dialog('close');updatedUserRegistered;$('#register').dialog('open');" /> 

Depends on three custom objects: a form object, a form field object, and a form preferences object (which holds the active form we want to embed on our page).

Currently supports JQuery Validation on the following types:

STRING
DOUBLE
URL
EMAIL
PHONE (US only)

For other types we use standard input fields:

BOOLEAN
DATETIME
PICKLIST
REFERENCE
DATE

I use the JQuery Sortable plugin to drag and drop the fields. There is, well, rather a lot of APEX code supporting it.

I was creating forms with the active Robot API like so:

	subForm.append(new FormElement(ElementType.LABEL, "text_input_lab1", "Resource Title:")));
	subForm.append(new FormElement(ElementType.INPUT, "text_input_title","")));

Then making simple assignments to my objects::

					if(elem.getProperty("name").equals("text_input_title"))
						res.setName(elem.getProperty("value"));

No validation! Since a quick Google search turns up very little about validation with Google Wave, I decided to put together my own Google Wave gadget that would validate all form input with the JQuery Validation Plugin.

Let’s jump into it. First we need to create a gadget that we can use as the form. Especially important is that we implement the callback function for handling a change in state, since that will allow us to establish bi-directional communication with our robot.

    function stateUpdated() {
      if(wave.getState().get('sentemail') == 'yes') {
      	 var viewerName = wave.getViewer().getDisplayName();
	 $("#commentForm").html("Feedback form submitted successfully. Thank you for your feedback " + viewerName);
      }
      else if(wave.getState().get('emailfailure') == 'yes') {
        $("#commentForm").html("There was a problem submitting your form. Please leave a comment at d3developer.com");
      }
    }

    function init() {
      if (wave && wave.isInWaveContainer()) {
        wave.setStateCallback(stateUpdated);
      }
    }
    gadgets.util.registerOnLoadHandler(init);

First we have the method that we want to call each time the state is changed. Because our Robot is using the Google App Engine email handler to process the form data, we want to make sure that the email is successfully sent before we update the user.

Then we set this method as the callback method for state changes with the line:

        wave.setStateCallback(stateUpdated);

Our form is pretty standard stuff. A name field, an email field, a comment field, and an optional url field.

To enable it we just include JQuery and the JQuery validate plugin:

    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.1/jquery.min.js"></script>
    <script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery.validate/1.6/jquery.validate.min.js"></script>
	<script type="text/javascript">

	$(document).ready(function() {

	$("#commentForm").validate({
	 submitHandler: function(form) {

		  // A couple lines to help us verify that all of the values
		  // on our form elements are what we want them to be
		  //$('.formelement').each(function(index) {
    	  //		alert($(this).attr("id") + '2: ' + $(this).val());
  		  //	});

	     var comment = $("#ccomment").val();
	     var name = $("#cname").val();
	     var url = $("#curl").val();
	     var email = $("#cemail").val();

	     wave.getState().submitDelta({'comment': comment,
	     							  'name' : name,
	     							  'url' : url,
	     							  'email' : email,
	     							  'sendemail': 'yes'});

		 //We never actually submit the form
	     //form.submit();

 	}

	});

If we implement the submitHandler we can add to and/or override the standard form submission. In this case, we don’t care about submitting the form, only passing the validated data to our robot for future processing. We do this with the submitDelta method.

Now all we have to do is add our Gadget to the wave somewhere like so:

		      Gadget gadget = new Gadget(FORM_GADGET_URL);
		      blip.append(gadget);

And then implement a method to react on a Gadget State Change (which will happen only if the form is successfully validated).

@Override
@Capability(contexts = {Context.SELF})
public void onGadgetStateChanged(GadgetStateChangedEvent e) {
	  	LOG.log(Level.WARNING, "OnGadgetStateChanged");
	  	Blip blip = e.getBlip();

	  	Gadget gadget = Gadget.class.cast(blip.first(ElementType.GADGET).value());

	  	LOG.log(Level.WARNING, "just looked for the gadget");

	    if(gadget!=null)
	    {
		  	LOG.log(Level.WARNING, "Found Gadget");
		  	if (gadget.getProperties() != null)
			  	LOG.log(Level.WARNING, "Gadget has properties");
	    }

	    String name = "";
	    String email = "";
	    String url = "";
	    String comment = "";

	    if (gadget != null &&
	            gadget.getProperty("sendemail", "no").equals("yes") &&
	            	gadget.getProperty("sentemail", "no").equals("no") ) {

		  	    LOG.log(Level.WARNING, "found sendemail");

		    	name = gadget.getProperty("name", "");
		    	email = gadget.getProperty("email", "");
		    	url = gadget.getProperty("url", "");
		    	comment = gadget.getProperty("comment", "");

	          Boolean success = sendEmail(name, email, url, comment);

	          if (success)
	        	  blip.first(ElementType.GADGET).updateElement(
		              ImmutableMap.of("sentemail", "yes"));
	          else
	        	  blip.first(ElementType.GADGET).updateElement(
			              ImmutableMap.of("emailfailure", "yes"));

	        }

}

Each time we update an element this will trigger a state change on the gadget which will then run the callback method we setup earlier. As you can see we also notify the user if there is a problem.

Here is our method for sending email:

public Boolean sendEmail(String name, String email, String url, String comment) {

	String recipientAddress = "feedbackrecipient@resourcy.appspot.com";

	Properties props = new Properties();
	Session session = Session.getDefaultInstance(props,null);

	try {
		Message message = new MimeMessage(session);
		// This address must be registered as a 'developer' with Google App Engine
		message.setFrom(new InternetAddress("dev@resourcy.appspot.com",
				name));
		message.addRecipient(Message.RecipientType.TO,
				new InternetAddress(recipientAddress));
		message.setSubject("Feedback Message from " + name);
		String body;
		if (url!=null && url.length() > 3)
			body = name + " (" + email + ")\n" + "from " + url + " \nsends the following comment: \n" + comment;
		else
			body = name + " (" + email + ") " + " \nsends the following comment: \n" + comment;

		message.setText(body);
		Transport.send(message);
		return true;

	} catch (AddressException e) {
		LOG.log(Level.WARNING, "Address is malformed: " + e);

	} catch (MessagingException e) {
		LOG.log(Level.WARNING, "Problem with mail service: " + e);

	} catch (UnsupportedEncodingException e) {
		LOG.log(Level.WARNING, "Problem with encoding: " + e);

	}

	return false;
}

Here is our form:

And after a successful submit here is what we get via email a few seconds later:

You can also vote for Resourcy in the ongoing contest at Mashable. Vote here!

The complete gadget code resides here.

After CloudForce in New York yesterday in the advanced track of the Developer Meetup Quinton Wall (@cloudcoder) shared some additional Chatter recipes as well as a few Force.com best practices. While no doubt some of these will make it up into the awesome new Chatter Commons Code Share, for those who can’t wait and couldn’t be there, here are a couple of those things:

First, a Partner WSDL and WSC jar for API v 18 was released yesterday and you can now access Chatter Feeds via the Partner API.

Second, apparently queries on feeds are much more efficient if you specify the FeedType (instead of FeedBody != null, as was included the first set of Recipes).

Those FeedTypes (from the Introduction to Chatter article) are:

  • TextPost (standard post)
  • LinkPost (a link)
  • UserStatus
  • ContentPost  (an attachment)
  • TrackedChanges (changes to tracked fields)

Also, Some objects (notably, for Chatter, EntitySubscription) will not allow duplicate records. If you try a standard insert (entitySub.insert) you will get an Exception.  However, there may be cases in your test code where you need to try to insert something regardless.

Try this:

// We want to insert but don't know if an EntitySub already exists

		EntitySubscription es = new EntitySubscription();
		es.parentId = 'objectId';
		es.subscriberId = 'id of user';

		//This will not throw an Exception even if the insert is unsuccessful

	    Database.SaveResult sr = Database.insert(es,false);

This helps you avoid encasing your insert statements in try/catch blocks (like I did!) and I believe originates from Steve Andersen of GoKubi fame.

Also not commonly known is that you can use the relatively new AggregateResult Object to capture results from a Count call. We might want to test our Chatter code like so:

		AggregateResult beforeCount = [select COUNT(id) ciid from UserFeed];

		//Add something which should go in the feed

		AggregateResult afterCount = [select COUNT(id) ciid from UserFeed];

		System.assertEquals( ( (Integer) beforeCount.get('ciid') + 1), (  (Integer) afterCount.get('ciid')));

Here’s another cool query that tracks User Activity over the past week:

SELECT
ParentId pid, Parent.Name pname, COUNT(id) fcount
FROM UserFeed
Where Type = 'UserStatus'
And CreatedDate = THISWEEK
GROUP BY Parent.Name, ParentID
ORDER BY COUNT(id) DESC LIMIT 10

There are somewhat more complicated ways of using AggregateResults that Quinton Wall is using in his soon to be released Chatter Recommendations app. Stay tuned!

Also presented were some test strategies:

  • Always test against governors (this is especially important with Chatter!)
  • Leverage the “grant login’ access
  • Use System.runAs(user)
  • Generic SObject Lists are your friend (you can also do this: SObject.getId() )
  • Use the ChatterTestHelper (e.g. ChatterTestHelper.getAdminUsers.get(0))

Oh, and don’t forget about the Chatter Developer Challenge, also announced last night.

Since Kyle Roche’s excellent introduction in September two important things have happened:

(1) Google Wave released a new Robot API (March 2, 2010)
(2) Salesforce launched the Developer Preview of Chatter (March 15, 2010)

So, you ask, why not bring all of these together in one nifty tutorial? Great idea!

If you’ve gotten the Hello World app on the Force.com for Google App Engine wiki working on your own or with our walkthrough for OS X you are in a good place to start working on the Google Wave connector for Chatter. If Chatter is new to you and you see objects (i.e. CustomObject__Feed) which look strange to you, you may want to reference the Chatter recipes for examples.

Here’s what we are going to do:

(1) Set up a Google Wave Robot
(2) Have the Google Wave Robot display custom objects from Salesforce and posts on those custom objects
(3) Save replies made in Google Wave as new posts on those custom objects in Salesforce

For this example, we will be using a site which aggregates tea listings from online tea vendors and allows people to review them and comment on their reviews. While working through it, try to imagine all of the other possibilities (e.g. managing customer feedback on cases, emulation of question/answers, etc.).

On to the Google Wave Robot:

First, if you don’t have a Google Wave Sandbox account, request one. Next, download the Wave Robot Java Client libraries.

Inside you should find recent versions of the following four jar files:

  • wave-robot-api.jar
  • gson.jar
  • oauth.jar
  • wave-model.jar

Copy them to the war/WEB-INF/lib directory of the project you just built. Right click on the project, then click Refresh. Right click again, go to properties, then Java Build Path. Add the four jars to your project. You should end up with something like this:

Although Google Wave Java tutorial gives us some cool things we can do with our robot we will skip ahead to what we need for our Chatter integration.

Create a second class called TeaoismRobot with the following code:


package com.joeldietz.teaoism;

import java.io.IOException;
import javax.servlet.http.*;
import java.util.*;
import com.google.wave.api.*;
import com.google.wave.api.event.*;

public class TeaoismRobot extends AbstractRobot {

  @Override
  protected String getRobotName() {
    return &quot;Teaoism Robot&quot;;
  }

  @Override
  public void onWaveletSelfAdded(WaveletSelfAddedEvent event) {
    Blip blip = event.getWavelet().reply(&quot;\nYou joined. That was fractastical!&quot;);
  }

}

Add this to your war/WEB-INF/appengine-web.xml file:

 &lt;precompilation-enabled&gt;true&lt;/precompilation-enabled&gt;

It should end up something like this:


&lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot;?&gt;
&lt;appengine-web-app xmlns=&quot;http://appengine.google.com/ns/1.0&quot;&gt;
 &lt;application&gt;yourApplicationName&lt;/application&gt;
 &lt;version&gt;1&lt;/version&gt;
 &lt;precompilation-enabled&gt;true&lt;/precompilation-enabled&gt;

 &lt;!-- Configure java.util.logging --&gt;
 &lt;system-properties&gt;
 &lt;property name=&quot;java.util.logging.config.file&quot; value=&quot;WEB-INF/logging.properties&quot;/&gt;
 &lt;/system-properties&gt;

&lt;/appengine-web-app&gt;

Add this to your war/WEB-INF/web.xml file:


    &lt;servlet&gt;
        &lt;servlet-name&gt;TeaoismRobot&lt;/servlet-name&gt;
        &lt;servlet-class&gt;com.joeldietz.teaoism.TeaoismRobot&lt;/servlet-class&gt;
    &lt;/servlet&gt;
    &lt;servlet-mapping&gt;
        &lt;servlet-name&gt;TeaoismRobot&lt;/servlet-name&gt;
        &lt;url-pattern&gt;/_wave/*&lt;/url-pattern&gt;
    &lt;/servlet-mapping&gt;

Now right click on your project and go to Google -> Deploy to App Engine.

Navigate to http://yourApplicationName.appspot.com/_wave/capabilities.xml and verify that the file has been automatically created. You should see something like this:

&lt;w:robot&gt;
&lt;w:version&gt;ffffffff840a022a&lt;/w:version&gt;
&lt;w:protocolversion&gt;0.2&lt;/w:protocolversion&gt;
−
&lt;w:capabilities&gt;
&lt;w:capability name=&quot;WAVELET_SELF_ADDED&quot;/&gt;
&lt;/w:capabilities&gt;
&lt;/w:robot&gt;

Now go to the Google Wave Sandbox. Click “New Wave” then the plus sign next to your picture to add a new participant to the wave. Type in “yourApplicationName@appspot.com” for the participant, hit enter, and verify that your bot responds after being added:

Cool. Now we jump into the juicy middle of our project. Let’s add a couple import statements:

import com.sforce.soap.enterprise.*;
import com.sforce.soap.enterprise.sobject.SObject;
import com.sforce.soap.enterprise.sobject.Review__c;
import com.sforce.soap.enterprise.sobject.Review__Feed;
import com.sforce.soap.enterprise.sobject.FeedPost;
import java.util.logging.Logger;
import java.util.logging.Level;

Review is our Chatter-enabled custom object for this example, and in order to access the Chatter feed for Review (Review__Feed), we need to generate our Enterprise library after we have Chatter-enabled this object. For more information about the enterprise library, check out the Force. wiki working on your own or with or our last post.

Assuming all is in order, we add a few variables:

  private static final long serialVersionUID = -1875649295661564335L;
  String SFDCusername = &quot;your username&quot;;
  String SFDCpasswordAndSecToken = &quot;your password plus security token&quot;;
  EnterpriseConnection connection = null;
  private static final Logger LOG = Logger.getLogger(TeaoismWaveRobot.class.getName());

Now let’s add a method for getting our Reviews from Salesforce:

  // Returns a list of which the first element is the review
  // and all subsequent elements are Chatter posts related to that review
  public ArrayList&lt;ArrayList &lt;String&gt;&gt; getReviews() {
     ArrayList&lt;ArrayList &lt;String&gt;&gt; outputStrings = new ArrayList();

	  try {
		   if ( connection == null )
		   {
			   ConnectorConfig config = new ConnectorConfig();
		       config.setUsername(username);
		       config.setPassword(password);
		       connection = Connector.newConnection(config);
		   }
		   QueryResult result = null;

		   	// This needs to be a chatter enabled object
		   	result = connection.query( &quot;SELECT Id, Tea_Listing__r.Tea_Standard_Name__r.Name, Tea_Listing__r.direct_URL__c, Tea_Listing__r.Vendor__r.name, Review_Text__c, Rating__c from Review__c limit 10&quot;);

	        for (int i=0; i &lt; result.getRecords().length;i++)
	        {

				ArrayList &lt;String&gt; reviewPlusPosts = new ArrayList &lt;String&gt;();

	        	Review__c review = (Review__c)result.getRecords()[i];

	        	// position 0
	        	reviewPlusPosts.add(review.getId().toString());

	        	String myReview = &quot;Tea name:&quot; + review.getTea_Listing__r().getTea_Standard_Name__r().getName() + &quot;\n\n&quot;;
	        	myReview += &quot;Sold by:&quot; + review.getTea_Listing__r().getVendor__r().getName() + &quot;\n\n&quot;;
	        	myReview += &quot;Review text:&quot; + review.getReview_Text__c() + &quot;\n\n&quot;;
	        	myReview += &quot;URL:&quot; + review.getTea_Listing__r().getDirect_URL__c() + &quot;\n\n&quot;;
	        	myReview += &quot;Rating:&quot; + review.getRating__c() + &quot;\n\n&quot;;

	        	// position 1
	        	reviewPlusPosts.add(myReview);

			   String queryString = &quot;SELECT Id, FeedPost.Body, CreatedDate from Review__Feed where FeedPost.Body != null and parentid='&quot; + review.getId() + &quot;'&quot;;
			   QueryResult postQR = connection.query(queryString);

		       for (int j = 0; j &lt; postQR.getRecords().length;j++)
		       {
		    	   Review__Feed rf = (Review__Feed)postQR.getRecords()[j];
		    	   String reviewPost = &quot;Post: &quot; + rf.getFeedPost().getBody() + &quot;\n\n&quot;;
		    	   reviewPost += &quot;Posted At: &quot; + rf.getCreatedDate().getTime();

		    	   reviewPlusPosts.add(reviewPost);

		       }

			   outputStrings.add(reviewPlusPosts);

	        } // outer for
	         LOG.log(Level.WARNING, &quot;OS:&quot; + outputStrings.size());

	  } catch (ConnectionException ce) {
		  ArrayList &lt;String&gt; errorMsg = new ArrayList &lt;String&gt;();
		  errorMsg.add(&quot;Connection Error:&quot; + ce.getMessage() + &quot;:&quot; + ce.getClass());
		  outputStrings.add(errorMsg);
	  }
	  return outputStrings;
}

For each review we are putting the id of the review in the first position (index 0), the body of the review in the second position (index 1), and all posts related to that review in all subsequent positions.

Although ideally we would be using a connection manager such as helpfully provided here, for the purposes of this demo we are simply checking if we have a connection already.

Here is where we output the generated code:


  @Override
  // The following line prevents us from sending extra data over the wire
  @Capability(contexts = {Context.SELF})
  public void onWaveletSelfAdded(WaveletSelfAddedEvent event) {

	 LOG.info(&quot;onWaveletSelfAdded&quot;);
      Wavelet wavelet = event.getWavelet();
      wavelet.setTitle(&quot;Fractastical!&quot;);
      wavelet.getRootBlip().append(&quot;\n&quot;);
	  //for kicks
      HashMap&lt;String, String&gt; imageProps = new HashMap&lt;String, String&gt;();
      imageProps.put(&quot;width&quot;, &quot;97&quot;);
      imageProps.put(&quot;height&quot;, &quot;78&quot;);
      imageProps.put(&quot;url&quot;, &quot;http://i909.photobucket.com/albums/ac295/fractastical/M_fractals_010-1.jpg&quot;);
      wavelet.getRootBlip().append(new Element(ElementType.IMAGE, imageProps));
      wavelet.getRootBlip().append(&quot;\nIt works! Now let's see some reviews!&quot;);

	  ArrayList&lt;ArrayList &lt;String&gt;&gt; reviews = getReviews();
	  for(ArrayList&lt;String&gt; r : reviews)
	  {

		  if (r.size() &gt; 1)
		  {
			  Blip b = wavelet.getRootBlip().reply();
			  b.all().annotate(&quot;teaoism.appspot.com/REVIEWID&quot;, r.get(0));
			  b.all().annotate(&quot;teaoism.appspot.com/ROBOT_GENERATED&quot;, &quot;TRUE&quot;);
			  b.append(r.get(1));

			for(Integer i=2;i &lt; r.size();i++)
			{
				Blip reply = b.reply();
				reply.all().annotate(&quot;REVIEWID&quot;, r.get(0));
				reply.all().annotate(&quot;ROBOT_GENERATED&quot;, &quot;TRUE&quot;);
				reply.append(r.get(i));

			}
		  }
		  else
			 for (String os : r)
				 wavelet.getRootBlip().append(os);
	  }
      wavelet.getRootBlip().append(&quot;\nThere they are!&quot;);

}

You will need to replace your previous onWaveletSelfAdded method with this one. The most basic component of the Google Wave Model is the blip. This method creates blips which contain reviews that are each replies of the root blip of the Wavelet, then creates blips that are replies of the review blips. Each blip is annotated with the review id and a note specifying that it was robot generated.

If we don’t have enough records, this indicates that something most likely went wrong with our queries or connection and we simply output whatever message we got back.

If you want, deploy this to the Google App engine and verify that it is correctly displaying all of your objects and related posts.

Next, the other direction. Here is a method that runs each time that a Blip is submitted:

  @Override
  @Capability(contexts = {Context.SELF, Context.PARENT})
 public void onBlipSubmitted(BlipSubmittedEvent e) {

	LOG.log(Level.WARNING, &quot;WaveletBlipCreatedEvent&quot;);
    Blip blip = e.getBlip();

    String robotGen = &quot;FALSE&quot;;
    for(Annotation a : blip.getAnnotations())
    {
    //We were unable to use the get(String name) method for Annotations so we have to iterate through them all
     if(a.getName() == &quot;teaoism.appspot.com/ROBOT_GENERATED&quot;)
    	 robotGen = a.getValue();
    }

    if (robotGen != &quot;TRUE&quot;)
    {
       	String reviewId = &quot;&quot;;

    	Blip parentBlip = blip.getParentBlip();

    	if (parentBlip != null)
    	{
	    	for(Annotation a : parentBlip.getAnnotations())
	        {
	         if(a.getName().equals(&quot;teaoism.appspot.com/REVIEWID&quot;))
	        	 reviewId = a.getValue();
	        }
    	}

        if (reviewId != &quot;&quot;)
        {
        	String result = insertFeedPost(reviewId, blip.getContent());
        	blip.append(&quot;\n\n&quot; + result);
        }
        else {
        	blip.append(&quot;A corresponding review could not be found for your comment&quot;);
        }

    }

  }

Here we check to see if the blip being submitted is generated by our robot. If it is not, we look to see if the blip that it is a reply of (the parent blip) has a review id. If it does, then we run our method to insert the new post.

This method is as follows:

public String insertFeedPost(String parentId, String body) {
	  String outputString = &quot;&quot;;
	  try {
		   if ( connection == null ) {
		    ConnectorConfig config = new ConnectorConfig();
		       config.setUsername(username);
		       config.setPassword(password);
		       connection = Connector.newConnection(config);
		   }
	  } catch (ConnectionException ce) {
		  outputString += ce.getMessage() + &quot;:&quot; + ce.getClass();
	  }
	  try {
		  FeedPost fp = new FeedPost();
		  fp.setParentId(parentId);
		  fp.setBody(body);
		  connection.create(new SObject[] { (SObject)fp });
		  outputString = &quot;Post inserted successfully.&quot;;
		}
		catch ( ConnectionException ce ) {
			outputString += ce.getMessage();
		}

	return outputString;

}

That’s all there is to it!

If you want to create a custom Visualforce page to display your Chatter-enabled custom object and related posts, here is some additional code.

First, for our controller:

   public class reviewCon {

    public List&lt;Review__c&gt; reviews {get; set;}
    public String rid {get; set;}

    public reviewCon() {
    
    }
    
    public void init() {
    
        reviews = [SELECT Id, Tea_Listing__r.Tea_Standard_Name__r.Name, Tea_Listing__r.direct_URL__c, Tea_Listing__r.Vendor__r.name, Review_Text__c, Rating__c from Review__c limit 10];
    
    }
    
    public List&lt;Review__Feed&gt; getReviewFeed()
    {
      return [SELECT Id, FeedPost.Body, CreatedDate from Review__Feed where FeedPost.Body != null and parentid=:rid];
    }

}

And our Visualforce page:

&lt;apex:page controller=&quot;reviewCon&quot; action=&quot;{!init}&quot;&gt; 

    &lt;apex:form&gt;
       &lt;apex:pageBlock id=&quot;rpb&quot;&gt;
         
           &lt;apex:pageBlockTable value=&quot;{!reviews}&quot; var=&quot;r&quot;&gt;
               &lt;apex:column headerValue=&quot;Name&quot;&gt;
                     &lt;apex:outputText value=&quot;{!r.Tea_Listing__r.Tea_Standard_Name__r.Name}&quot; /&gt;
               &lt;/apex:column&gt;
               
               &lt;apex:column headerValue=&quot;Review Text&quot;&gt;
                     &lt;apex:outputText value=&quot;{!r.Review_Text__c}&quot; /&gt;
               &lt;/apex:column&gt;
               
               &lt;apex:column headerValue=&quot;Review Text&quot;&gt;
                   &lt;apex:commandLink value=&quot;Show Feed&quot; reRender=&quot;reviewfeedblock&quot;&gt;
                      &lt;apex:param name=&quot;rid&quot; value=&quot;{!r.id}&quot; assignTo=&quot;{!rid}&quot;/&gt;
                   &lt;/apex:commandLink&gt;
               &lt;/apex:column&gt;
           
           &lt;/apex:pageBlockTable&gt;
      
       
           &lt;apex:pageBlockSection title=&quot;Review Feed&quot; id=&quot;reviewfeedblock&quot; columns=&quot;1&quot;&gt;
              &lt;apex:pageBlockTable value=&quot;{!reviewFeed}&quot; var=&quot;f&quot; columns=&quot;1&quot; border=&quot;0&quot;&gt;
    
                &lt;apex:column headerValue=&quot;Posts on this item&quot;&gt;
                     &lt;apex:outputText value=&quot; {!f.FeedPost.Body}&quot; id=&quot;feedbody&quot;/&gt;
                &lt;/apex:column&gt;
    
            &lt;/apex:pageBlockTable&gt;
            
         &lt;/apex:PageBlockSection&gt;
         
       &lt;/apex:pageBlock&gt;
       
    &lt;/apex:form&gt;


 &lt;/apex:page&gt;

@fractastical updates

 

April 2010
M T W T F S S
« Mar   May »
 1234
567891011
12131415161718
19202122232425
2627282930  
Follow

Get every new post delivered to your Inbox.

Join 1,312 other followers