Showing posts with label best practices. Show all posts
Showing posts with label best practices. Show all posts

Sunday, December 6, 2015

Keep it simple stupid: an example from Comcast

I know its been a while since my last blog post, and when I look back at some of my older posts, I see so much I would like to update or correct. But isn't that always the way when we look back on our work? If its from a few years ago or a few months ago, since we are always learning from each task we take on, looking back on any of our work should bring with it insight on how it could be better. I think thats one of the most important and amazing parts of being in this engineering role, being able to look at something (a process, a website, a user interface, a system, a toy, whatever) and see how to improve it. Or at the very least recognize that it could be improved and that there is something not quite right about it.

Lets look at an example of something, that to me, isn't quite right. I happen to be a customer of Comcast or as its also know Xfinity. Aside from any personal experiences with the company you may have, they are a large company and have a large user base. They need to make sure their user interfaces, (the remote, the guide, on demand etc) are clear and simple for all audiences to be able to know exactly what to do on a page right when they land on it. It needs to be out right obvious what the purpose of the page is and to guide the user, making it easy to use and thus creating a good user experience. I've watched them through updates, each time removing some clunkiness from the overall user process, but generally just changing the look and feel. This being said, it is still one of the better TV interfaces I've seen, taking queues from Hulu and Netflix and merging it with their own blend of stuff. It feels like they are making progress but missing some of the small wins that would make it better as a whole, and round off the user experience. 

I want to share one such example that to me says a lot about whats wrong with so many user interfaces out there. In this one example I can see more than a few things 'wrong' but lest see if you agree with me. 

Here is the prompt in Comcast that pops up when you finish watching a recording:


When looking at something, as a user, I should be able to glance and with less than a fraction of a second be able to know where to go next. But, when I first look at this, I see the word 'Delete' in BOTH of the buttons! So instantly I am forced to take an extra second to then read the text and options again. My next thought is why are there more than one word in each button? Couldn't we keep it simple and use just 'Delete' and 'Save'? Instead we are a) confusing the user with delete in each button and b) making the verbiage more complicated than it needs to be. The next issue I see is that the potentially negative action is selected by default. By doing this, if the user miss reads the buttons (and as I have outlined that is a very possible outcome) they may just click on the first selected option assuming that its the action they want because it is highlighted by default. I would think that as an added precaution to the user to have to switch over to the 'Delete' button would be one more affirmation that, YES I do want to delete this. We could go one step further and color the text if we wanted with a red for Delete and some sort of neutral color for Save, but in general just switching these buttons to be single words and not using delete in each text would help immensely.

Such a small change for such a big impact. 

I use this example because I mistakenly deleted a recording while my toddler was distracting me, and it was the recording that he wanted to watch again! After I tempered his tantrum, my first thought was to look for some sort of 'deleted recordings' queue, maybe they would give me a window of time to change my mind and undelete, but alas they do not.. at least not that I could find. But it got me thinking about why it happened, and how it could have been prevented. It also reminds me to keep it simple in my own work, and try to remove this sort of clunkiness from any process I may encounter. I only use this example because of how easily it can be identified with, we have all been there, and how easily it can be translated to all of our own it work. Lets take a lesson from this and continue to strive to bring better interfaces to our end users! 

Thursday, February 2, 2012

Detecting the sObject type of Id's from ambiguous fields

When dealing with the OwnerId field, it is important to check if it is a User or a Group sObject you are dealing with before you start using it or you'll get errors. The Task object also has a few ambiguous fields that match up multiple Objects like the WhoId and the WhatId. In triggers and in other logic you will sometimes need to detect what the object is to either exclude or include it in the next bit of code. We dont want to iterate through code and update records if we don't need to, it will bog down the system. 


I have seen some of the ways out there that are used, particularly the one where you check if the first 3 letters of the ID is '500' or '300' but i do not think this is an accurate way of detection. I want one that the system tells me for sure what object it is. Unfortunately its not as dynamic as i would like it to be due to limitations in APEX, but it is still a much better detection than the above. 


I first made a helper class called GlobalHelper_IsSobjectType and populated it with a few methods that look like the following. It takes in the Id and after a check to make sure that an Id was actually passed and is not null, it try's to create an Object of that Type. If it can it will not fall into the Catch and will return true, other wise it will error and return false, but it will not error in a way that will halt your program from finishing(thus why its in a try catch). 




I made one for Account, Contact, Lead, User, and Opportunity since those are the ones I typically have to detect. When it comes to the OwnerId field I can just use the one for User since if it returns false i know its a group. I also found that I needed to Overload the method so it would work with string as well as Id: 




Ideally I was hoping to make the above but make it more dynamic so you pass the object type you wanted to detect as well as the id for the detecting. That way you would only need one method instead of one for each Object, but I have yet to find a way to make it work. Now of course you need to test the code, and I prefer as close to 100% coverage as possible so:






Questions?  
Twitter: @SalesForceGirl  or Facebook


Wednesday, February 1, 2012

APEX Trigger to Prevent Duplicate Object Records

One of the biggest issues in Org's that I have seen is with duplicate records. But what is the best way to prevent this? With an easy trigger of course! 


Let take the Contact object, in my Org, everything is keyed off email, so we shouldn't have more than one contact with the same email address. And we need to not only protect against new Contacts, but we also need to ensure that if a User updates the email on the Contact that it too is not already in use before the update occurs. In order for the User to know that they are trying to Insert or Update incorrectly we will have a field error show next to the email of the contact stating that one already exists.


Lets make(or update) a Contact trigger for before insert and before update. 






We are going to use a Map to store the id and email of each contact that comes into the trigger so we can use that to do a SOQL query later when its time to see if it already exists.  add the 'trigger.isBefore' even though we know this will only occur before, so that this can be easily extended in the future and we wont have to go back and change code.




Now we need to populate the map with the Contact info so we can use it in the SOQL query next. We should also send up an error if the batch of Contacts that is being iterated over in the trigger contains more than one of the same email. 




Now that we have the Map populated, or at least in theory it should be, we will query the DB to see if a Contact already exists with that email, and if so, show an error to the User, other wise do nothing and allow it to be inserted/updated. 






And thats it. You can of course make this much more robust, so it not only compares email but also name and phone number too. 


Questions?  
Twitter: @SalesForceGirl  or Facebook

Wednesday, December 28, 2011

easy double-click to edit related-list VF page

Often enough I am asked to make a custom VisualForce of a related list that is editable in a similar way to salesforce's native double click to edit. I have made them in a variety of ways, including adding images like salesforce's to indicate if a field is editable or locked etc. and some simpler ways like the one I am going to do now.

The Problem:
a sObject under Account(or any other obj) needs show on the Account page layout, but it also needs to be double-click to edit to make it easy for the reps to update key information on the fly without having to click into each sObject. (for this example we will use the Contact obj)


Setup:
First we need to create a controller extension so that it can sit in the page layout of the Account obj, and even though we are showing the contact obj, the extension needs to point to account otherwise it wont show up as an available VisualForce page to add on the layout.
ContactInLineEdit.cls
ContactInLineEdit.page

This is a standard controller extension setup, this allows the class to use the account id referenced in the URL when the page is accessed. Example page URL will be 'apex/ContactInLineEdit?id={!account.id}'

Now we should add the Contact list to the class so we can access it on the page in a apex:repeat, when we add it to the class, we only need to get the list on load of the page, and then again when we update the fields.
ContactInLineEdit.cls

On the page we will need to add the apex:form tag, along with an apex:outputpanel with an id so we can call it later in a rerender attribute on the update call. We will also add the CSS and jQuery files that will help us control the double-click to edit functionality. We then place the jQuery inside the outputpanel so when it rerenders it can reapply the jQuery to the page. I also prefer to use the jQuery.noConflict(); since I often run into issues with salesforce's JS conflicting with what I am trying to implement.
ContactInLineEdit.page

Now we need to add the apex:pagemessages so we can do error handling and we need to add the table for the apex:repeat to sit in. We will also add the apex:outputfiled's that will display in the related list on the page, we need to make sure that any field we reference here is also in the select query on the class.
ContactInLineEdit.page

Lets add in the loading icon for the update and a JS function to control it, and we should also add the edit fields to the table. To help control the conditional hiding and showing of the fields for the edit, we should add some div's with classes to reference in jQuery.
ContactInLineEdit.page
ContactInLineEdit.page

Lets add the double click functionality, first we need to add a class to the edit text in the action column  <span class="editLink">Edit</span> then we need to add a function so that everytime the nonEdit field (outputField) is double-clicked it will hide the outputField and show the inputField. We will also change the Edit text to Cancel and add a class to it so we know that row is in edit mode.

Now lets add the edit row/cancel row edits functionality, this will allow all editable fields in the row to be edited and if cancel is hit, it will undo all edits by retrieving the text from the hidden outputfield otherwise it will continue to show the updated text since we are just hiding and showing. We don't want to clear the fields cause that will potentially clear out the saved info if it gets saved by mistake. So by retrieving the info from the outputfield, we are able to set it back to what it was originally.
ContactInLineEdit.page

We will also need to update our edit action to use the new function, it will now look like this: <span class="editLink" onclick="EditMe(jQuery(this));">Edit</span>
At this point we need to make the update method and add it to the page, this will allow any edits made by the user to be saved, and then the list will rerender with the new information.
ContactInLineEdit.cls

ContactInLineEdit.page

And that it, simple. I didn't add it here, but field validation could be added to ensure its a proper email or phone number etc. In the style i made so that fields that were editable when the mouse hovered over it, the cursor would change to a pointer like it would when the mouse hovers over a link. This way the user knows the field is editable instead of having to add an image that could bog down the page load.

Questions?  
Twitter: @SalesForceGirl  or Facebook

Tuesday, May 31, 2011

What makes Elegant Code?

One of the things i love about programming is that there is always several different ways to accomplish a task. If you gave 10 programmers the same task, each would go about it in there own way, sure the result would be the same but the code will be completely unique.

I find that when i look at other programmers code, its almost like looking at an artist work, not to say that the code is a work of art, just unique and sometimes beautiful. Every programmer has there own way of doing things, tendencies that all make up the big picture of the code. The naming convention used, structure of the methods and even the way they went about accomplishing the task. It all contributes to the look and feel of the code. It also can show the skill of the programmer or artist, or lack there of. 

Code should be simplistic, elegant, beautiful. It should also be structured, and still 'flow' like a picture. So many times i look at code and often think of spaghetti, and cringe at the over complication of something that should only be one or two lines and not ten. And please don't get me wrong, i am always trying to learn new ways to improve my code, trying to find smarter ways to go about a problem, keeping an open mind to new ideas and concepts.

An example of what i mean, below is two lines, both of the same SOQL statement, both get the job done. But one, while not wrong, also isn't quite right.

Contact[] objContact = [select id, name from Contact where email ='someemail@email.com' limit 1];

Contact objContact = [select id, name from Contact where email = :sInEmail limit 1];

In the first SOQL statement, it is creating a list of Contacts even though the query is only returning one. It also has a 'hard-coded' variable which is never a good practice.  In the second, since we are only querying for one obj, it only creates a single object, also it uses a variable instead of string text. 

Now i know what you may be thinking, does that really make a difference in the elegance of the code? Yes. If we left it as the first statement, where it is returning a List of objs instead of a single obj, and all the proceeding methods would then have to accommodate for it, even thought it isn't necessary. Its like adding lines and logic that only complicate and so the code tends to look messy or clustered when it doesn't need to be. Keeping code simplistic is key. As a rule of thumb, I try to keep my methods under 10 lines total, and that includes all the curly brackets.

Here is another example of doing things different ways while still accomplishing the same task:

      public string mystring;

      public string getMystring()
      {
            return mystring;
      }

      public void setMyString(string s)
      {
            this.s = mystring;
      }

vs

          public string sInStringName{get;set;}

I hope I don't need to explain why the second is cleaner, but if you notice i use the variable name of 'sInStringName' instead of 'mystring', i do this for most variable's: 'lst' for list, 'obj' for sObjects, etc. this helps me identify what it is, along with what type. So if i am looking for a string, i know it will start with 's'. I also like to follow the camel hump approach where the first word in the name is lowercase and all words after have the first letter capitalized. When it comes to booleans i am still trying to decide if i like 'b' as in 'bInBoolean' or 'is' since it is a true false statement, which would look like 'isBoolean'. Its important that no matter what naming convention you choose, that you stick with it, for the whole project, if you don't like it, try tweaking it in the next project.

Questions? 

Twitter: @SalesForceGirl  or Facebook

Friday, April 1, 2011

Knowledge's new multi-lingual support: A brief look

In my last post I briefly described that in the Spring'11 release Salesforce updated knowledge with new multi-lingual support. While it is good in theory, in practice it presents some problems that are hard to work with.

One of the issues of the old knowledge version is in mass importing docs that may have changed or updated. This is due to the required unique URL Name field, and there being no way to update, only add as 'new'. To get around this in the old version, our edit team had to append -es or -de to the end of the field to allow

Example of our old update process:
  • receive the zip file from Doc team (iOS_Ignition_ES_update2.zip) and unpack.
  •  edit the CSV file to include:
    •  datacategorygroup.products (lmiignitioniphone)
    • a unique Doc_Group__c (igniosuges)
    • and an edit to the URL name to prevent overlap/conflict with similar products (What-is-the-Host-ios-es2)
  •  then re-zip files and add the articleImportSampleContent.properties file
  •  In Salesforce, search for articles tagged with the same Doc_Group__c (igniosuges) and remove them from published docs due to URL name issue
  •  In Saleforce, under Data Management > Import Articles, choose the new zip file and import.
  •  After import, can then verify/review docs in draft and publish.
With the new knowledge multi-lingual support, we hoped it would solve this issue, but it does not, at least not for our needs.

When you follow their instructions for doing export, you'll find that they don't spell everything out, leaving you to assume some of the details, or having to run thought and figure it out by trial and error.

They also provide a 4min 'how-to' video, which walks you though the process of setting up the new multi-lingual support and the two ways they offer for translations, but it is more of an overview, and misses a few key issues with the import, mainly that the URL Name field needs to be Unique so it cant be the same as the original Article.

Then when your trying to do an export, besides being very cumbersome, the format of the export itself is not very usable, It would be nice to be able to export to .csv since you can import with it, but instead it gives you a zip of a bunch of folders with an individual document in each. If you try to export form the translation work bench, on the page where you choose what format you wish to export in, it says:

You can export three kinds of text
* Source - Produces one .csv file that contains all of the text that is translatable
* Untranslated - Produces a set of files, by language, with text that is not yet translated
* Bilingual - Produces a set of files by language that contain all of the text that is translatable

BUT the source does NOT produce a .csv file, it is instead a .stf file, which i am sure is useful for the correct program, but not what we need, or expected from the above instructions.

Overall i still think that knowledge's new multi-lingual features is a step in the right direction. It is definitely good if you are doing in-house editing, BUT I think that knowledge, in general, has a ways to go if they want orgs with more than 500 articles to utilize it efficiently.

Friday, March 18, 2011

Global Helper Classes - Top 5 Methods to Include

Check for Null Value
Its always important to check for null, and since the check for null is the same over and over, it should be a Helper method. Since salesforce doesn't have a generic type variable that covers all like JS does with the type 'var' or C# does with the Type 'T'. Sure they have sObject, so we can do generic objects, but nothing that covers everything like strings, Lists etc. So in my Helper class I overloaded the method for each 'type' I need to check for null in. As you can see they are essentially the same method just checking a different type of variable.
/// <summary>
/// OVERLOADED
/// CHECKS IF STRING IS NULL OR EMPTY
/// </summary>
/// <param name="sInValueToCheck">STRING TO CHECK</param>
/// <returns>TRUE IF NOT NULL</returns>
public static boolean CheckForNotNull(string sInValueToCheck)
{
      if(sInValueToCheck != null && sInValueToCheck != '' && sInValueToCheck.toLowerCase() != 'null')
      {
            return true;
      }
      return false;
}
/// <summary>
/// OVERLOADED
/// CHECKS IF SOBJECT LIST IS NULL OR EMPTY
/// </summary>
/// <param name="lstInValueToCheck">STRING TO CHECK</param>
/// <returns>TRUE IF NOT NULL</returns>
public static boolean CheckForNotNull(List<sobject> lstInValueToCheck)
{
      if(lstInValueToCheck != null && lstInValueToCheck.size() > 0)
      {
            return true;
      }
      return false;
}

Get Param
I cant tell you how many times i need to get a param from the page, so it just made scene to include it here. As you should all know, when using params you should always reduce the case to lower and trim the param to ensure that when you go to compare it to what the value should be, it is always able to be matched up. But, Salesforce Id's are case-sensitive, so I created two Methods, one to reduce the case to lower, and a normal one. I couldn't overload the method since each has the same signature, plus this makes it obvious which one is the toLower() one. Another method i have, but am not showing here, isUrlParamOrDefault(string sInUrlKey, string sInDefualt), this enables you to always return a value instead of null.... well except of course if the sInDefault isn't null.
/// <summary>
/// GETS THE PARAM VALUE BASED ON KEY FROM URL
/// </summary>
/// <param name="sInUrlKey">URL PARAM KEY</param>
/// <returns>PARAM VALUE TO LOWER</returns>
public static string UrlParamToLower(string sInUrlKey)
{
      if(GlobalHelper.CheckForNotNull(sInUrlKey))
      {
            String UrlParam = ApexPages.currentPage().getParameters().get(sInUrlKey);
            if(GlobalHelper.CheckForNotNull(UrlParam))
            {
                  UrlParam = UrlParam.toLowerCase().trim();
            }
            return UrlParam;
      }
      return null;
}

/// <summary>
/// OVERLOADED
/// GETS THE PARAM VALUE BASED ON KEY FROM URL
/// </summary>
/// <param name="sInUrlKey">URL PARAM KEY</param>
/// <returns>PARAM VALUE</returns>
public static string UrlParam(string sInUrlKey)
{
      if(GlobalHelper.CheckForNotNull(sInUrlKey))
      {
            return ApexPages.currentPage().getParameters().get(sInUrlKey);
      }
      return null;
}

Check Value
Its always important to not only check for null, but to also make sure its the value you expect. So with these methods your able to check to see if the value passed is the same as the string passed, or if it is in the List that was passed. 
/// <summary>
/// OVERLOADED
/// CHECK URL PARAM AGAINST LIST TO ENSURE IT IS WHAT IT SHOULD BE
/// </summary>
/// <param name="sInUrl">URL PARAM</param>
/// <param name="lstInValueToCompare">LIST TO COMPARE VALUE AGAINST</param>
/// <returns>TRUE IF CORRECT VALUE ELSE FALSE</returns>
public static boolean CheckValue(string sInUrl, List<string> lstInValueToCompare)
{
      if(GlobalHelper.CheckForNotNull(lstInValueToCompare))
      {
            for(string s : lstInValueToCompare)
            {
                  if(GlobalHelper.CheckForNotNull(sInUrl))
                  {
                        if(sInUrl.toLowerCase().trim() == s.toLowerCase().trim())
                        {
                              return true;
                        }
                  }
            }
      }
      return false;
}
/// <summary>
/// OVERLOADED
/// CHECK URL PARAM AGAINST STRING TO ENSURE IT IS WHAT IT SHOULD BE
/// </summary>
/// <param name="sInUrl">URL PARAM</param>
/// <param name="sInToCompare">VALUE TO COMPARE AGAINST</param>
/// <returns>TRUE IF CORRECT VALUE ELSE FALSE</returns>
public static boolean CheckValue(string sInUrl, string sInToCompare)
{
      if(GlobalHelper.CheckForNotNull(sInUrl))
      {
            if(sInUrl.toLowerCase().trim() == sInToCompare.toLowerCase().trim())
            {
                  return true;
            }
      }
      return false;
}

Comparison (de-dupe)
When creating lists, its important to de-dupe to ensure uniqueness(if thats what is called for in your list). What these methods do is to check the current value passed against the list you want to add it to, ensuring that the passed value inst in the list. Honestly this is used constantly in my code to ensure the list doesn't contain duplicate values.
/// <summary>
/// OVERLOADED
/// CHECKS IF STRING IS IN LIST
/// </summary>
/// <param name="sInCurrentValue">STRING TO CHECK</param>
/// <param name="lstInCollectionValue">LIST TO COMPARE VALUE TO</param>
/// <returns>TRUE IF IN LIST</returns>
public static boolean ContainsItem(String sInCurrentValue, List<String> lstInCollectionValue)
{
      if(GlobalHelper.CheckForNotNull(lstInCollectionValue))
      {
            for(String s: lstInCollectionValue)
            {
                  if(GlobalHelper.CheckForNotNull(sInCurrentValue))
                  {
                        if(sInCurrentValue.toLowerCase().trim() == s.toLowerCase().trim())
                        {
                              return true;
                        }
                  }
            }
      }
      return false;
}
/// <summary>
/// OVERLOADED
/// CHECKS IF SOBJECT IS IN LIST
/// </summary>
/// <param name="objInCurrent">SOBJECT TO CHECK</param>
/// <param name="lstInCollection">LIST TO COMPARE VALUE TO</param>
/// <returns>TRUE IF IN LIST</returns>
public static boolean ContainsItem(sobject objInCurrent, List<sobject> lstInCollection)
{
      if(GlobalHelper.CheckForNotNull(lstInCollection))
      {
            for(sobject s: lstInCollection)
            {
                  if(s != null && objInCurrent != null)
                  {     
                        string sId = s.Id;
                        string cId = objInCurrent.id;
                        if(GlobalHelper.CheckForNotNull(cId) && GlobalHelper.CheckForNotNull(sId))
                        {
                              if(sId.toLowerCase().trim() == cId.toLowerCase().trim())
                              {
                                    return true;
                              }
                        }
                  }
            }
      }
      return false;
}

Add items to list
The Comparison methods would be used in something like the following method, which would return the completed list of items, de-duped. These methods allow you to either create a List<String> of Id's, or a List<sObject>, but it ensures that the list will be 'cleaned out' since it is using the Helper method of 'ContainsItem'
/// <summary>
/// OVERLOADED
/// ADDS SOBJECT IDS TO STRING LIST AFTER DEDUPING
/// </summary>
/// <param name="lstInSobjectValue">LIST TO GET VALUES FROM</param>
/// <returns>STRING LIST OF SOBJECT IDS</returns>
public static List<String> AddItemsToIdList(List<sobject> lstInSobjectValue)
{
      List<String> idList = new List<string>();
      if(GlobalHelper.CheckForNotNull(lstInSobjectValue))
      {
            for(sobject obj: lstInSobjectValue)
            {
                  if(obj != null)
                  {
                        if(!GlobalHelper.ContainsItem(obj.id, idList))
                        {
                              idList.add(obj.Id);
                        }
                  }
            }
            return idList;
      }
      return null;
}
/// <summary>
/// OVERLOADED
/// ADDS SOBJECT TO LIST AFTER DEDUPING
/// </summary>
/// <param name="lstInsObjectItems">LIST TO GET VALUES FROM</param>
/// <returns>SOBJECT LIST</returns>
public static List<sObject> AddItemsToList(List<sObject> lstInsObjectItems)
{
      List<sObject> newList = new List<sObject>();
      if(GlobalHelper.CheckForNotNull(lstInsObjectItems))
      {
            for(sObject con : lstInsObjectItems)
            {     
                  if(con != null)
                  {
                        if(!GlobalHelper.ContainsItem(con, newList))
                        {
                              newList.Add(con);
                        }
                  }
            }
            return newList;         
      }
      return null;
}

Questions? 
Twitter: @SalesForceGirl  or Facebook