Wednesday, December 08, 2010

ASP.NET MVC Master-Detail List view with a LINQ twist


The source code for this blog posting can be downloaded from this link. MasterDetailsWithALinqTwist.zip

I was asked the following question:


"I agree with that person that told you that is difficult to find a good sample for Master Detail views, in particular for people who are begining with ASP.NET MVC 2.
Your post is a quit good intent to help people like me, but I think it's a little complicated for beginners.

I try to find in Google and in specialized books an answer to a simple but, I think, a very usual problem. I'll explain it with an example:

3 files:

1- Books:
with BookID (int, key), BookTitle (string)

2- Authors:
with AuthorId (int, key), AuthorName (string)

3- AuthorBook
with AuthorId, BookId

The last file is a "junction file". We need it because a book has one or more auhtors.

The problem, which solution I can't find it anywhere, is how can I make a list with the book title with its authors, but not repeating the book title.

Thank you for your time,"

This question was posted as a comment on my blog posting ASP MVC 2.0 - Master Detail Views

First I would like to point out that normally in Object Relational Mapping on the OO side you normally do not create objects for the one-many or many-many relations present in the database - unless the relations themselves carry special data or they have special properties and thus can be promoted to "real" objects/classes.

This means that I will ignore the third file mentioned above in the question (3- AuthorBook with AuthorId, BookId).

So I tend to see the above as a Book class with a property IList Authors, like below

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MvcApplication2.Models
{
 public class Book
 {
  public string Id { get; set; }
  public string Name { get; set; }
  public IList<Author> Authors { get; set; }
 }
}

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MvcApplication2.Models
{
 public class Author
 {
  public string Id { get; set; }
  public string Name { get; set; }
 }
}

For this small sample code I did not bother to wire the code to a database but just created a LibraryRepository class that can be seen below, this class acts as a placeholder for a database.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace MvcApplication2.Models
{
 public class LibraryRepository
 {
  public IList<Book> GetBooks()
  {
   List<Book> books = new List<Book>() 
   {
    new Book() 
    {
     Id = "1",
     Name = "My First Book",
     Authors = new List<Author>() 
     {
      new Author() { Id = "A1", Name = "Author1" },
      new Author() { Id = "A2", Name = "Author2" }
     }
    },
    new Book() 
    {
     Id = "2",
     Name = "My Second Book",
     Authors = new List<Author> () 
     {
      new Author() { Id = "A3", Name = "Author3" },
      new Author() { Id = "A4", Name = "Author4" }
     }
    }
   };
   return books;
  }
 }
}

Now that we have the "database" and the models in place then we need the controller and the view. Ideally the LibraryRepository should be injected in to the controllers constructor using Dependency Injection (which I am not doing here).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using MvcApplication2.Models;

namespace MvcApplication2.Controllers
{
 [HandleError]
 public class HomeController : Controller
 {
  public ActionResult Index()
  {
   // Should be passed in via IOC and contructor injection
   LibraryRepository libraryRepository = new LibraryRepository();
   ViewData.Model = libraryRepository.GetBooks();
   return View();
  }
 }
}

And finally the strongly typed view that uses a nifty LINQ expression to aggregate the Authors into a single string for display purposes.

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<MvcApplication2.Models.Book>>" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
 Index
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Index</h2>

    <table>
        <tr>
            <th></th>
            <th>
                Id
            </th>
            <th>
                Name
            </th>
            <th>
                Authors
            </th>
        </tr>

    <% foreach (var item in Model) { %>
    
        <tr>
            <td>
                <%: Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) %> |
                <%: Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ })%> |
                <%: Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })%>
            </td>
            <td>
                <%: item.Id %>
            </td>
            <td>
                <%: item.Name %>
            </td>
            <td>
                <%: item.Authors.Select(author => author.Name).Aggregate((currentAuthorName, nextAuthorName) => currentAuthorName + ", " + nextAuthorName) %>
            </td>
        </tr>
    
    <% } %>

    </table>

    <p>
        <%: Html.ActionLink("Create New", "Create") %>
    </p>

</asp:Content>

1. Select the names from the Author object into an anonymous object of type
System.Linq.Enumerable.WhereSelectListIterator<MvcApplication2.Models.Author,string>

2. Aggregate the names in the anonymous object (System.Linq.Enumerable.WhereSelectListIterator<MvcApplication2.Models.Author,string>) adding a comma between.

The output ends up like the following.



The MVC design pattern argues that the views should be pretty lightweight and should only deal with presenting data, logic related to readying data for display / and actual formatting of data should ideally be located in the controller ( I am not doing that here ).

Saturday, December 04, 2010

Refactoring ASP MVC 2 to use Dependecy Injection (part2)


This is a continuation of my previous post Refactoring ASP MVC 2 to use Dependecy Injection (part1)

The main layers I would like in my application are the following


LayerContainsDepends on/using
GuiLayer(partial) views, user controls, jsPresentationLayer, (DomainLayer)
PresentationLayerviewmodels, controllers, MVC plumbingDomainLayer
DomainLayerdomain objects, business logicnothing
DataLayerLINQ generated classes, repository handlers ...DomainLayer

When we look at the dependencies it is worth noting that the DomainLayer does not depend on anything. The DomainLayer contains all our domain logic and relevant objects and we want to be able to swap the GUILayer but also the DataLayer (see more Domain-driven design @ Wikipedia)

Let us have a look at the domain model I have defined.


For clarity I have omitted several other classes. Since both the domain model and the database was created from the actual scenario we are trying to solve here they are remarkably similar, however this is not a requirement (it just makes the DB-OO mapping easier).

Since the PresentationLayer contains view models that are relevant to the way we present our data we might find that we have several view models for one domain model or one view model that is composed by more than one domain model. We do not want the domain models to know about the view models since they might change depending on a rich client GUI that allows more data to be shown, but then again they might not, the key point is that we do not know and neither does the DomainLayer.

To this effect I have added an interface that is used in the PresentationLayer (view models).
Please see sample implementations for the 3 view models.


using System;
using System.Collections.Generic;

namespace ReverseAuction.Models
{
    interface IDomainModelToViewModelConverter<T, U>
    {
        U ConvertDomainModelToViewModel(T domainEntity);
        List<U> ConvertDomainModelToViewModel(List<T> domainEntities);
    }
}

The reason for letting the ViewModels handle the conversion from DomainModel to ViewModel (this also goes for the data layer - the DataModels handle the conversion from DataModel to DomainModel) is that the domain model is the interface all other layer know about.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Appinux.ReverseAuction.Domain;

namespace Appinux.ReverseAuction.Models
{
    public class SimpleUserViewModel : IDomainModelToViewModelConverter<User, SimpleUserViewModel>
    {
        public string Username { get; set; }
        public Guid ID { get; set; }

        public SimpleUserViewModel ConvertDomainModelToViewModel(User domainEntity)
        {
            this.ID = domainEntity.ID;
            this.Username = domainEntity.Username;
            return this;
        }


        public List<SimpleUserViewModel> ConvertDomainModelToViewModel(List<User> domainEntities)
        {
            List<SimpleUserViewModel> viewModelList = new List<SimpleUserViewModel>();

            foreach (User domainEntity in domainEntities)
            {
                viewModelList.Add(new SimpleUserViewModel().ConvertDomainModelToViewModel(domainEntity));
            }
            return viewModelList;
        }
    }
}

More of the same here
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Appinux.ReverseAuction.Domain;

namespace Appinux.ReverseAuction.Models
{
    public class ProjectBidViewModel : IDomainModelToViewModelConverter<ProjectBid, ProjectBidViewModel>
    {
        public SimpleUserViewModel Bidder { get; set; }
        public Double BidAmount { get; set; }
        public DateTime BidDate { get; set; }

        public ProjectBidViewModel ConvertDomainModelToViewModel(ProjectBid domainEntity)
        {
            this.BidAmount = domainEntity.Amount;
            this.BidDate = domainEntity.PostedDate;
            this.Bidder = new SimpleUserViewModel().ConvertDomainModelToViewModel(domainEntity.PostedBy);
            return this;
        }


        public List<ProjectBidViewModel> ConvertDomainModelToViewModel(List<ProjectBid> domainEntities)
        {
            List<ProjectBidViewModel> viewModelList = new List<ProjectBidViewModel>();

            foreach (ProjectBid domainEntity in domainEntities)
            {
                viewModelList.Add(new ProjectBidViewModel().ConvertDomainModelToViewModel(domainEntity));
            }
            return viewModelList;
        }
    }
}

and the last class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Appinux.ReverseAuction.Domain;

namespace Appinux.ReverseAuction.Models
{
    public class ProjectViewModel : IDomainModelToViewModelConverter<Project, ProjectViewModel>
    {
        public string Name { get; set; }
        public string Description { get; set; }
        public SimpleUserViewModel Owner { get; set; }
        public DateTime BiddingStartDate { get; set; }
        public DateTime BiddingEndDate { get; set; }
        public DateTime ProjectLauchDate { get; set; }
        public DateTime ProjectDeadline { get; set; }
        public double MaxAllowedBid { get; set; }
        public string Status { get; set; }
        public List<projectbidviewmodel> ProjectBids { get; set; }

        public ProjectViewModel ConvertDomainModelToViewModel(Project domainEntity)
        {
            this.Name = domainEntity.Name;
            this.Description = domainEntity.Description;
            this.Owner = new SimpleUserViewModel().ConvertDomainModelToViewModel(domainEntity.Owner);
            this.BiddingStartDate = domainEntity.BiddingStartDate;
            this.BiddingEndDate = domainEntity.BiddingEndDate;
            this.ProjectLauchDate = domainEntity.StartDate;
            this.ProjectDeadline = domainEntity.Deadline;
            this.MaxAllowedBid = domainEntity.MaxBidAmount;
            this.Status = domainEntity.Status.Name;
            this.ProjectBids = new ProjectBidViewModel().ConvertDomainModelToViewModel(domainEntity.Bids);
            
            return this;
        }


        public List<ProjectViewModel> ConvertDomainModelToViewModel(List<Project> domainEntities)
        {
            List<ProjectViewModel> viewModelList = new List<ProjectViewModel>();

            foreach (Project domainEntity in domainEntities)
            {
                viewModelList.Add(new ProjectViewModel().ConvertDomainModelToViewModel(domainEntity));
            }
            return viewModelList;
        }
    }
}

I have on purpose done a non optimal structuring of the code above. Before continuing have a look at Single responsibility principle.

The problem:
According to the Single responsibility principle a class should have one responsibility. This is always up to interpretation what is the specific class's responsibility. However since ViewModels per definition are responsible for carrying data from the controller to the View in ASP.NET MVC, they should not know about conversion between DomainModels and themselves.

Depending on the complexity of the mapping between ViewModels and DomainModels & DomainModels and DataModels Factory classes could improve the modularity and adherence to the Single responsibility principle.

So lets fix this.

We are going to split each of the ViewModels into a ViewModel containing the DTO / POCO and a static Factory class.

Now the classes look like the following:

using System;

namespace Appinux.ReverseAuction.Models
{
 public class SimpleUserViewModel
 {
  public string Username { get; set; }
  public Guid ID { get; set; }
 }
}

using System;
using System.Collections.Generic;
using Appinux.ReverseAuction.Domain;

namespace Appinux.ReverseAuction.Models
{
 public static class SimpleUserViewModelFactory
 {
  public static SimpleUserViewModel Create(User domainEntity)
  {
   SimpleUserViewModel viewModel = new SimpleUserViewModel();
   viewModel.ID = domainEntity.ID;
   viewModel.Username = domainEntity.Username;
   return viewModel;
  }

  public static List<SimpleUserViewModel> Create(List<User> domainEntities)
  {
   List<SimpleUserViewModel> viewModelList = new List<SimpleUserViewModel>();
   foreach (User domainEntity in domainEntities)
   {
    viewModelList.Add(SimpleUserViewModelFactory.Create(domainEntity));
   }
   return viewModelList;
  }
 }
}

using System;

namespace Appinux.ReverseAuction.Models
{
 public class ProjectBidViewModel
 {
  public SimpleUserViewModel Bidder { get; set; }
  public Double BidAmount { get; set; }
  public DateTime BidDate { get; set; }
 }
}

using System;
using System.Collections.Generic;
using Appinux.ReverseAuction.Domain;

namespace Appinux.ReverseAuction.Models
{
 public static class ProjectBidViewModelFactory
 {
  public static ProjectBidViewModel Create(ProjectBid domainEntity)
  {
   ProjectBidViewModel viewModel = new ProjectBidViewModel();
   viewModel.BidAmount = domainEntity.Amount;
   viewModel.BidDate = domainEntity.PostedDate;
   viewModel.Bidder = SimpleUserViewModelFactory.Create(domainEntity.PostedBy);
   return viewModel;
  }

  public static List<ProjectBidViewModel> Create(List<ProjectBid> domainEntities)
  {
   List<ProjectBidViewModel> viewModelList = new List<ProjectBidViewModel>();
   foreach (ProjectBid domainEntity in domainEntities)
   {
    viewModelList.Add(ProjectBidViewModelFactory.Create(domainEntity));
   }
   return viewModelList;
  }
 }
}

using System;
using System.Collections.Generic;

namespace Appinux.ReverseAuction.Models
{
 public class ProjectViewModel
 {
  public string Name { get; set; }
  public string Description { get; set; }
  public SimpleUserViewModel Owner { get; set; }
  public DateTime BiddingStartDate { get; set; }
  public DateTime BiddingEndDate { get; set; }
  public DateTime ProjectLauchDate { get; set; }
  public DateTime ProjectDeadline { get; set; }
  public double MaxAllowedBid { get; set; }
  public string Status { get; set; }
  public List<ProjectBidViewModel> ProjectBids { get; set; }
 }
}

using System;
using System.Collections.Generic;
using Appinux.ReverseAuction.Domain;

namespace Appinux.ReverseAuction.Models
{
 public static class ProjectViewModelFactory
 {
  public static ProjectViewModel Create(Project domainEntity)
  {
   ProjectViewModel viewModel = new ProjectViewModel();
   viewModel.Name = domainEntity.Name;
   viewModel.Description = domainEntity.Description;
   viewModel.Owner = SimpleUserViewModelFactory.Create(domainEntity.Owner);
   viewModel.BiddingStartDate = domainEntity.BiddingStartDate;
   viewModel.BiddingEndDate = domainEntity.BiddingEndDate;
   viewModel.ProjectLauchDate = domainEntity.StartDate;
   viewModel.ProjectDeadline = domainEntity.Deadline;
   viewModel.MaxAllowedBid = domainEntity.MaxBidAmount;
   viewModel.Status = domainEntity.Status.Name;
   viewModel.ProjectBids = ProjectBidViewModelFactory.Create(domainEntity.Bids);
   return viewModel;
  }

  public static List<ProjectViewModel> Create(List<Project> domainEntities)
  {
   List<ProjectViewModel> viewModelList = new List<ProjectViewModel>();
   foreach (Project domainEntity in domainEntities)
   {
    viewModelList.Add(ProjectViewModelFactory.Create(domainEntity));
   }
   return viewModelList;
  }
 }
}

Let us look at how the traditional layering of tiered applications often look like.


Now lets look at how the same application usually looks like after a few iterations of feature additions based on new requirements from the business.
The technical debt increases, and a normal consequence is that the number of dependencies increase.



My proposal to let the entire stack of layers revolve around the domain layer means that the presentation layer does not have any indirect dependecy on anything else than the DomainLayer. The same goes for the DataLayer. This is actually how it works in most businesses, the business requirements are the driving factor for development and IMHO from a development standpoint the application should have the DomainLayer as the driver (since the DomainLayer should be the software equivalent of the buisiness requirements) (I know this is not the full story since there
often also are UI requirements etc ... but let us for now, arrogantly :), ignore these.

The picture below shows the dependencies in the approach I am suggesting here.



The whole point about interface based design is that you can swap classes with other classes that implement the same interface.

However if you new up specific classes in methods then you are adding a hard dependecy on the class. The code below show how a lot of code looks. This is a contrived example in real life applications it is never one single new statement but many all over the place. The classes needed to implement functionality often resides in several assemblies / namespaces. This means that you very quicly get into dependecy hell.

And on a side note it makes Test Driven Development much more difficult since you cannot mock the dependecies.

using System;
using System.Web.Mvc;
using SomeApplication.Domain;

namespace SomeApplication.Domain
{
 public interface ISomeInterface 
 {
  string DoSomething();
 }

 public class SomeClass : ISomeInterface
 {
  public string DoSomething()
  {
   throw new NotImplementedException();
  }
 }

}

namespace SomeApplication.Presentation
{
 public class HomeController : Controller
 {
  public HomeController()
  { 
  }
  
  public ActionResult Index()
  {
   // Bad Bad Bad - this is adding a dependency directly to the SomeApplication.Domain.SomeClass
   ISomeInterface someInteface = new SomeClass();
   return View();
  }
 }
}

Let us try to take away the knowledge about what specific class that is being used in the controller in the sample above.

using System;
using System.Web.Mvc;
using SomeApplication.Domain;

namespace SomeApplication.Domain
{
 public interface ISomeInterface 
 {
  string DoSomething();
 }

 public class SomeClass : ISomeInterface
 {
  public string DoSomething()
  {
   throw new NotImplementedException();
  }
 }

}

namespace SomeApplication.Presentation
{
 public class HomeController : Controller
 {
  private ISomeInterface someInterface;
  public HomeController(ISomeInterface someInterface)
  { 
   this.someInterface = someInterface;
  }
  
  public ActionResult Index()
  {
   // Now this class knows nothing about the concrete class, this knowledge is pushed higher up the call stack
   someIterface.DoSomething();
   return View();
  }
 }
}

The above way of structuring code is called constructor injection. In addition there are property injection and method injection. This is not really anything new. Since the advent of OO interface based design this approach have been recommended, but now this specific way of coding OO have a name Dependency injection.

The highest point in the call stack with regards to Dependency Injection is normally called the Composition Root. Composition Root is normally located as close to the application start as possible, so for an ASP.NET application this would normally be the Global.asax.

That the subject of the next post in this series. How to manually implement Dependency Injection in ASP.NET MVC.

Thursday, December 02, 2010

SHIFT Click javascript selection

I found myself needing a standard shift click selection functionality JavaScript for a website.  Preferably it should encapsulate the actual SHIFT click logic so it would be easy to use and reuse. It should provide the same or similar functionality of windows explorer or Konqueror. I was not able to find anything that I felt suited my need so I rolled my own. It is based on JQuery. You can see a demo of the Shift Click Javascript selection here.

I have tested it in Firefox 3.6, Chrome5/6 and IE8.

First the accompanying index.html page

<html>
<head>
<link href="style.css" rel="Stylesheet" type="text/css">
<title></title>
</head>
<body>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript" charset="utf-8"></script>
<script src="ShiftClick.js" type="text/javascript" charset="utf-8"></script>
<script src="main.js" type="text/javascript" charset="utf-8"></script>
<ul id="my-list">
    <li data-id="1">
        <div class="checkbox"></div>
        <h4 title="My name" class="name">My item1</h4>
    </li>
    <li data-id="2">
        <div class="checkbox"></div>
        <h4 title="My name" class="name">My item2</h4>
    </li>
    <li data-id="3">
        <div class="checkbox"></div>
        <h4 title="My name" class="name">My item3</h4>
    </li>
    <li data-id="4">
        <div class="checkbox"></div>
        <h4 title="My name" class="name">My item4</h4>
    </li>
    <li data-id="5">
        <div class="checkbox"></div>
        <h4 title="My name" class="name">My item5</h4>
    </li>
</ul>
</body>
</html>

and now the style.css file

#my-list li { height: 16px; position: relative; cursor: pointer; margin: 10px 0;}

#my-list li div, 
#my-list li h4 { position: absolute; overflow: hidden; left: 5px; margin: 0; top: 0;}

#my-list li div.checkbox { border-style:solid; border-width:1px; border-color:#fff; cursor: pointer; left: -25px; height: 17px; width: 17px; background: url(check-mark.png) no-repeat 0 0;}
#my-list li.checked div.checkbox { background-position: -16px 0; }

the main.js file

/**
* Solution specific file that is using the ShiftClick closure for both UI modification as well as the actualy
* SHIFT clicking logic
*/
var state = { previouslySelected: undefined, previouslyShiftSelected: undefined };



// "Your own click handler"
function onItemClick(elmTarget) {
 // Call the ShiftClick closure and let that handle the heavy lifting (passing in the callback as the last argument)
 ShiftClick.itemClick(elmTarget, state, '#my-list li', 'checked', handleItemClick);
 
 // INPUT YOUR CODE HERE
}

// Your own handleItemClick
function handleItemClick(elmTarget, selectedClass, itemChecked) {
 // Call the ShiftClick closure to do the UI updating
 ShiftClick.updateItemUI(elmTarget, selectedClass, itemChecked);
 
 // INPUT YOUR CODE HERE ... calling a backend service or logging ...
}



// JQuery extend to avoid html dom text to be selected
// Shamelessly copied from http://stackoverflow.com/questions/1319126/prevent-highlight-of-text
jQuery.fn.extend({ 
 disableSelection : function() { 
   this.each(function() { 
     this.onselectstart = function() { return false; }; 
     this.unselectable = "on"; 
     jQuery(this).css('-moz-user-select', 'none'); 
   }); 
 } 
});

$(document).ready(function(){
 // disable selection on #li text
 $('#my-list li').disableSelection();

 // Setup the "Your own click handler" for the selector
 $('#my-list li .checkbox').click( onItemClick );
});

and the ShiftClick closure/lib

/**
* Utility closure that implements range selection in a list of html elements
* SHIFT clicking
*/
(function (window, $, undefined) {
 var CHECKED = 'checked';
 var UNCHECKED = 'unchecked';
 
 /**
 * Implements the familiar SHIFT click functionality which can be found in most UI programs that allow
 * item selection. SHIFT clicking selects a range from the first item to the second item clicked
 */
 function itemClick(elmTarget, state, itemsSelector, selectedClass, handleItemClickCallback) {
  var startLoopIndex, endLoopIndex, previousSelectedIndex, previousShiftSelectedIndex, currentlySelectedIndex, items;

  if (elmTarget.shiftKey) {
   var selectedItems = $(itemsSelector + '.' + selectedClass);
   
   // The user is selecting the second item in the range
   if (selectedItems.length) {
    items = $(itemsSelector);
    previousSelectedIndex = items.index(state.previouslySelected);
    currentlySelectedIndex = items.index( $(elmTarget.target).closest('li'));

    // Clear previous range if any
    if (state.previouslyShiftSelected !== undefined) {
     previousShiftSelectedIndex = items.index(state.previouslyShiftSelected);
     startLoopIndex = Math.min(previousShiftSelectedIndex, currentlySelectedIndex);
     endLoopIndex = Math.max(previousShiftSelectedIndex, currentlySelectedIndex);

     for (var i = startLoopIndex; i <= endLoopIndex; i++) {
      handleItemClickCallback && handleItemClickCallback(items.eq(i), selectedClass, UNCHECKED);
     }
    }
    
    // Select current range
    startLoopIndex = Math.min(previousSelectedIndex, currentlySelectedIndex);
    endLoopIndex = Math.max(previousSelectedIndex, currentlySelectedIndex);

    for (var i = startLoopIndex; i <= endLoopIndex; i++) {
     handleItemClickCallback && handleItemClickCallback(items.eq(i), selectedClass, CHECKED);
    }
   }
   // The user have not normal clicked an item before and starts with SHIFT clicking
   else {
    handleItemClickCallback && handleItemClickCallback(elmTarget.target, selectedClass);
   }
   state.previouslyShiftSelected = $(elmTarget.target).closest('li');
  }
  
  // Normal clicking
  else {
   state.previouslySelected =  $(elmTarget.target).closest('li');

   // Reset previously SHIFT selected items
   state.previouslyShiftSelected = undefined;
   handleItemClickCallback && handleItemClickCallback(elmTarget.target, selectedClass);
  }
 }

 /**
 * Handles the actual UI modification to make the UI reflect the items selected
 * Handles one item at a time
 */
 function updateItemUI(elmTarget, selectedClass, itemChecked, selectCallback) {
  
  var elmParentLi = $(elmTarget).closest('li');
  if (itemChecked == CHECKED) {
   elmParentLi.addClass(selectedClass);
  }
  else if (itemChecked == UNCHECKED) {
   elmParentLi.removeClass(selectedClass);
  }
  else {
   elmParentLi.toggleClass(selectedClass);
  }

  var checked = elmParentLi.hasClass(selectedClass);
  selectCallback && selectCallback(elmParentLi.attr('data-id'), checked);
 }

 /**
 * Public methods
 */
 ShiftClick = {
  itemClick: itemClick,
  updateItemUI: updateItemUI
 };

})(window, jQuery);

Wednesday, November 24, 2010

SugarCrm - Change max size of default name field in module builder

The following posting and the indicated modifications to code is based on the 5.5.2 release of SugarCrm CE edition.

Objective
SugarCRM allows defining Max Size for "Text Field" field types in Module Builder. We need to add feature to define/change html display size attribute for the Text Field from Module Builder. 

The changes will affect following fields:
1.    For custom Text Field
2.    For pre-defined TextField i.e.  name

Purpose
By default the display size for the Text Field is 30 characters but it can hold up to 255 characters. When you are creating or editing a record in EditView some times if the text you are editing in a text field is long enough it becomes awkward. This problem can be avoided by increasing the display size of the TextField.

Currently the TextField for name field in EditView will be rendered as:

<input type="text" maxlength="" size="30" id="name" title="" value="" name="name" />

I would like to be able to change this when I define a new module in Module Builder.


This change is not upgrade safe.

Files need to be changed

(I have marked the hack with comments in each individual section below)

1.    /modules/DynamicFields/DynamicField.php
function addFieldObject(&$field){
        $GLOBALS['log']->debug('adding field');
        $object_name = $this->module;
        $db_name = $field->name;
        
        $fmd = new FieldsMetaData();
        $id =  $fmd->retrieve($object_name.$db_name,true, false);
        $is_update = false;
        $label = $field->label;
        if(!empty($id)){
            $is_update = true;
        }else{
            $db_name = $this->getDBName($field->name);
            $field->name = $db_name;
        }
         $this->createCustomTable();
        $fmd->id = $object_name.$db_name;
        $fmd->custom_module= $object_name;
        $fmd->name = $db_name;
        $fmd->vname = $label;
        $fmd->type = $field->type;
        $fmd->help = $field->help;
        //Hack:Allow for changing the individual textboxes html size setting
        $fmd->size = $field->size;
        //Hack:Allow for changing the individual textboxes html size setting
        if (!empty($field->len))
           $fmd->len = $field->len; // tyoung bug 15407 - was being set to $field->size so changes weren't being saved
        $fmd->required = ($field->required ? 1 : 0);
        $fmd->default_value = $field->default;
        $fmd->ext1 = $field->ext1;
        $fmd->ext2 = $field->ext2;
        $fmd->ext3 = $field->ext3;
        $fmd->ext4 = (isset($field->ext4) ? $field->ext4 : '');
        $fmd->comments = $field->comment;
        $fmd->massupdate = $field->massupdate;
        $fmd->importable = ( isset ( $field->importable ) ) ? $field->importable : null ;
        $fmd->duplicate_merge = $field->duplicate_merge;
        $fmd->audited =$field->audited;
        $fmd->reportable = ($field->reportable ? 1 : 0);
        if(!$is_update){
            $fmd->new_with_id=true;
        }
        $fmd->save();
        $this->buildCache($this->module);
        if($field){
            if(!$is_update){
                $query = $field->get_db_add_alter_table($this->bean->table_name . '_cstm');
            }else{
                $query = $field->get_db_modify_alter_table($this->bean->table_name . '_cstm');
            }
            if(!empty($query)){
                $GLOBALS['db']->query($query);
            }
        }
        return true;
    }

function addField($name,$label='', $type='Text',$max_size='255',$size='',$required_option='optional', $default_value='', $ext1='', $ext2='', $ext3='',$audited=0, $mass_update = 0 , $ext4='', $help='',$duplicate_merge=0, $comment=''){
        require_once('modules/DynamicFields/templates/Fields/TemplateField.php');
        $field = new TemplateField();
        $field->label = $label;
        if(empty($field->label)){
            $field->label = $name;
        }
        $field->name = $name;
        $field->type = $type;
        $field->len = $max_size;
        $field->required = (!empty($required_option) && $required_option != 'optional');
        $field->default = $default_value;
        $field->ext1 = $ext1;
        $field->ext2 = $ext2;
        $field->ext3 = $ext3;
        $field->ext4 = $ext4;
        $field->help = $help;
        //Hack:Allow for changing the individual textboxes html size setting
        $field->size = $size;
        //Hack:Allow for changing the individual textboxes html size setting
        $field->comments = $comment;
        $field->massupdate = $mass_update;
        $field->duplicate_merge = $duplicate_merge;
        $field->audited = $audited;
        $field->reportable = 1;
        return $this->addFieldObject($field);
    }


2.    /modules/DynamicFields/templates/Fields/TemplateField.php
class TemplateField{
    /*
        The view is the context this field will be used in
        -edit
        -list
        -detail
        -search
    */
    var $view = 'edit';
    var $name = '';
    var $vname = '';
    var $id = '';
    // Hack:Allow for changing the individual textboxes html size setting
    var $size = '';
    // Hack:Allow for changing the individual textboxes html size setting

    var $len = '255';
    var $required = false;
    var $default = null;
    var $default_value = null;

function get_field_def(){
        $array =  array(
            'required'=>$this->required,
            'source'=>'custom_fields',
            'name'=>$this->name,
            'vname'=>$this->vname,
            'type'=>$this->type,
            'massupdate'=>$this->massupdate,
            'default'=>$this->default,
            'comments'=> (isset($this->comments)) ? $this->comments : '',
            'help'=> (isset($this->help)) ?  $this->help : '',
            // Hack:Allow for changing the individual textboxes html size setting
            'size'=>$this->size,
            // Hack:Allow for changing the individual textboxes html size setting
            'importable'=>$this->importable,
            'duplicate_merge'=>$this->duplicate_merge,
            'duplicate_merge_dom_value'=>$this->duplicate_merge, //nsingh: bug #14897 The UI did not get updated according to $app_list_strings['custom_fields_merge_dup_dom'], so include an extra field for the dom and leave the original duplicate_merge logic intact.
            'audited'=>($this->audited ? 1 : 0),
            'reportable'=>($this->reportable ? 1 : 0),
            );
            if(!empty($this->len)){
                $array['len'] = $this->len;
            }
            $this->get_dup_merge_def($array);
            return $array;
    }

3.    /modules/DynamicFields/templates/Fields/Forms/varchar.tpl

{else}
        <input type='hidden' name='len' value='{$vardef.len}'>{$vardef.len}
    {/if}
    </td>
</tr>

{* Hack:Allow for changing the individual textboxes html size setting *}
<tr>
    <td class='mbLBL'>{$MOD.COLUMN_TITLE_SIZE}: </td>
    <td>
        <input type="text" name="size" value="{$vardef.size}">
    </td>
</tr>
{* Hack:Allow for changing the individual textboxes html size setting *}
{include file="modules/DynamicFields/templates/Fields/Forms/coreBottom.tpl"}

4.    /modules/EditCustomFields/EditCustomFields.php

function add_custom_field($name, $label, $data_type, $max_size,$size,
        $required_option, $default_value, $deleted, $ext1, $ext2, $ext3, $audited, $mass_update=0, $duplicate_merge=0, $reportable = true)
    {
        $module_name = $this->module_name;

        $fields_meta_data = new FieldsMetaData();
        $fields_meta_data->name = $name;
        $fields_meta_data->label = $label;
        $fields_meta_data->module = $module_name;
        $fields_meta_data->data_type = $data_type;
        $fields_meta_data->max_size = $max_size;
        //Hack:Allow for changing the individual textboxes html size setting
        $fields_meta_data->size = $size;
        //Hack:Allow for changing the individual textboxes html size setting    
        $fields_meta_data->required_option = $required_option;
        $fields_meta_data->default_value = $default_value;
        $fields_meta_data->deleted = $deleted;

5.    /modules/EditCustomFields/EditView.html

<tr><td nowrap="nowrap">{MOD.COLUMN_TITLE_DATA_TYPE}:</td><td><select name="data_type" id='data_type' onchange="typeChanged(this);" {NOEDIT}>{data_type_options}</select></td></tr>

<tr><td nowrap="nowrap">{MOD.COLUMN_TITLE_MAX_SIZE}:</td><td><input type="text" name="max_size" value="{max_size}" /></td></tr>

<!-- Hack: Allow for changing the individual textboxes html size setting -->
<tr><td nowrap="nowrap">{MOD.COLUMN_TITLE_SIZE}:</td><td><input type="text" name="size" value="{size}" /></td></tr>
<!-- Hack: Allow for changing the individual textboxes html size setting -->    

<tr><td nowrap="nowrap">{MOD.COLUMN_TITLE_REQUIRED_OPTION}:</td><td><input type="checkbox" name="required_option" value="{required_option}" {REQUIRED_CHECKED}/></td></tr>

<tr><td nowrap="nowrap">{MOD.COLUMN_TITLE_DEFAULT_VALUE}:</td><td><input type="text" name="default_value" value="{default_value}" /></td></tr>

6.    /modules/EditCustomFields/EditView.php

$xtpl->assign('NOEDIT', 'disabled');
    $xtpl->assign('custom_field_id', $focus->id);
    $xtpl->assign('name', $focus->name);
    $xtpl->assign('label', $focus->label);
    $xtpl->assign('custom_module', $focus->custom_module);
    
   $data_type_options_html = get_select_options_with_id($data_type_array,
        $focus->data_type);
    
    $xtpl->assign('data_type_options', $data_type_options_html);
    $xtpl->assign('max_size', $focus->max_size);
    //Hack:Allow for changing the individual textboxes html size setting
    $xtpl->assign('size', $focus->size);
    //Hack:Allow for changing the individual textboxes html size setting
    $xtpl->assign('required_option', $focus->required_option);
    if($focus->required_option == 'required'){
        $xtpl->assign('REQUIRED_CHECKED', 'checked');
    }
    $xtpl->assign('default_value', $focus->default_value);
    
    $xtpl ->assign('ENUM_OPTIONS', get_select_options_with_id($enum_keys, $focus->ext1));
    $xtpl->assign('ext1', $focus->ext1);
    $xtpl->assign('ext2', $focus->ext2);
    $xtpl->assign('ext3', $focus->ext3);

7.    /modules/EditCustomFields/Forms.html

{mod.COLUMN_TITLE_DATA_TYPE}<br />
<select name="data_type">{data_type_options}</select><br />
{mod.COLUMN_TITLE_MAX_SIZE}<br />
<input type="text" name="max_size" value="" /><br />
<!-- Hack: Allow for changing the individual textboxes html size setting -->
{mod.COLUMN_TITLE_SIZE}<br />
<input type="text" name="size" value="" /><br />
<!-- Hack: Allow for changing the individual textboxes html size setting -->
{mod.COLUMN_TITLE_REQUIRED_OPTION}<br />
<input type="checkbox" name="required_option" value="1" /><br />
{mod.COLUMN_TITLE_DEFAULT_VALUE}
<input type="text" name="default_value" value="" /><br />

8.    /modules/EditCustomFields/ListView.html

<td scope="col" ><a href="{ORDER_BY}data_type" class="listViewThLinkS1"
    >{MOD.COLUMN_TITLE_DATA_TYPE}{arrow_start}{data_type_arrow}{arrow_end}</a></td>
<td scope="col" ><a href="{ORDER_BY}max_size" class="listViewThLinkS1"
    >{MOD.COLUMN_TITLE_MAX_SIZE}{arrow_start}{max_size_arrow}{arrow_end}</a></td>
<!-- Hack: Allow for changing the individual textboxes html size setting -->
<td scope="col" class="listViewThS1"><a href="{ORDER_BY}size" class="listViewThLinkS1">{MOD.COLUMN_TITLE_SIZE}{arrow_start}{size_arrow}{arrow_end}</a></td>    
<!-- Hack: Allow for changing the individual textboxes html size setting -->    
<td scope="col" ><a href="{ORDER_BY}required_option" class="listViewThLinkS1"
    >{MOD.COLUMN_TITLE_REQUIRED_OPTION}{arrow_start}{required_option_arrow}{arrow_end}</a></td>
<td scope="col" ><a href="{ORDER_BY}default_value" class="listViewThLinkS1"
    >{MOD.COLUMN_TITLE_DEFAULT_VALUE}{arrow_start}{default_value_arrow}{arrow_end}</a></td>
<td scope="col" >&nbsp;</td>

9.    /modules/EditCustomFields/Popup.html

tr>
<td nowrap="nowrap">{mod.COLUMN_TITLE_DATA_TYPE}</td>
<td><select name="data_type">{data_type_options}</select></td>
</tr>

<tr>
<td nowrap="nowrap">{mod.COLUMN_TITLE_MAX_SIZE}</td>
<td><input type="text" name="max_size" value="{max_size}" /></td>
</tr>
<!-- Hack: Allow for changing the individual textboxes html size setting -->
<tr>
<td nowrap="nowrap">{mod.COLUMN_TITLE_SIZE}</td>
<td><input type="text" name="size" value="{size}" /></td>
</tr>
<!-- Hack: Allow for changing the individual textboxes html size setting -->

<tr>
<td nowrap="nowrap">{mod.COLUMN_TITLE_REQUIRED_OPTION}</td>
<td><input type="checkbox" name="required_option" value="{required_option}" /></td>
</tr>

10.    /modules/EditCustomFields/Save.php

if(isset($_REQUEST['ext2'])){        
    $ext2 = $_REQUEST['ext2'];
}
$ext3 = '';
if(isset($_REQUEST['ext3'])){        
    $ext3 = $_REQUEST['ext3'];
}
$max_size = '255';
if(isset($_REQUEST['max_size'])){        
    $max_size = $_REQUEST['max_size'];
}
//Hack:Allow for changing the individual textboxes html size setting
if(isset($_REQUEST['size'])){        
    $size = $_REQUEST['size'];
}
//Hack:Allow for changing the individual textboxes html size setting
$required_opt = 'optional';
if(isset($_REQUEST['required_option'])){
    $required_opt = 'required';
}
$default_value = '';
if(isset($_REQUEST['default_value'])){
    $default_value = $_REQUEST['default_value'];
}

11.    /modules/EditCustomFields/vardefs.php

'fields' => array (
        'id'=>array('name' =>'id', 'type' =>'varchar', 'len'=>'255', 'reportable'=>false),
        'name'=>array('name' =>'name', 'vname'=>'COLUMN_TITLE_NAME', 'type' =>'varchar', 'len'=>'255'),
        'vname'=>array('name' =>'vname' ,'type' =>'varchar','vname'=>'COLUMN_TITLE_LABEL',  'len'=>'255'),
        'comments'=>array('name' =>'comments' ,'type' =>'varchar','vname'=>'COLUMN_TITLE_LABEL',  'len'=>'255'),
        'help'=>array('name' =>'help' ,'type' =>'varchar','vname'=>'COLUMN_TITLE_LABEL',  'len'=>'255'),
        'custom_module'=>array('name' =>'custom_module',  'type' =>'varchar', 'len'=>'255', ),
        'type'=>array('name' =>'type', 'vname'=>'COLUMN_TITLE_DATA_TYPE',  'type' =>'varchar', 'len'=>'255'),
        'len'=>array('name' =>'len','vname'=>'COLUMN_TITLE_MAX_SIZE', 'type' =>'int', 'len'=>'11', 'required'=>false, 'validation' => array('type' => 'range', 'min' => 1, 'max' => 255),),
        // Hack: Allow for changing the individual textboxes html size setting
        'size'=>array('name' =>'size' ,'type' =>'varchar','vname'=>'COLUMN_TITLE_SIZE',  'len'=>'255'),
        // Hack: Allow for changing the individual textboxes html size setting
        'required'=>array('name' =>'required', 'type' =>'bool', 'default'=>'0'),
        'default_value'=>array('name' =>'default_value', 'type' =>'varchar', 'len'=>'255', ),
        'date_modified'=>array('name' =>'date_modified', 'type' =>'datetime', 'len'=>'255',),        
        'deleted'=>array('name' =>'deleted', 'type' =>'bool', 'default'=>'0', 'reportable'=>false),
        'audited'=>array('name' =>'audited', 'type' =>'bool', 'default'=>'0'),        
        'massupdate'=>array('name' =>'massupdate', 'type' =>'bool', 'default'=>'0'),    
        'duplicate_merge'=>array('name' =>'duplicate_merge', 'type' =>'short', 'default'=>'0'),  
        'reportable' => array('name'=>'reportable', 'type'=>'bool', 'default'=>'1'),

12.    /modules/EditCustomFields/language/en_us.lang.php

'LBL_REPORTABLE'=>'Reportable',
    'ERR_RESERVED_FIELD_NAME' => "Reserved Keyword",
    'ERR_SELECT_FIELD_TYPE' => 'Please Select a Field Type',
    'LBL_BTN_ADD' => 'Add',
    'LBL_BTN_EDIT' => 'Edit',
    'LBL_GENERATE_URL' => 'Generate URL',
    'LBL_DEPENDENT_CHECKBOX'=>'Dependent',
    'LBL_DEPENDENT_TRIGGER'=>'Trigger',
    'LBL_BTN_EDIT_VISIBILITY'=>'Edit Visibility',
    // Hack: Allow for changing the individual textboxes html size setting
    'COLUMN_TITLE_SIZE' => "Size",
    // Hack: Allow for changing the individual textboxes html size setting
);
?>

13.    /modules/ModuleBuilder/MB/MBField.php

class MBField{
    var $type = 'varchar';
    var $name = false;
    var $label = false;
    var $vname = false;
    //Hack:Allow for changing the individual textboxes html size setting
    var $size = false;
    //Hack:Allow for changing the individual textboxes html size setting
    var $options = false;
    var $length = false;
    var $error = '';
    var $required = false;
    var $reportable = true;
    var $default = 'MSI1';

function getFieldVardef(){
        if(empty($this->name)){
            $this->error = 'A name is required to create a field';
            return false;
        }        
        if(empty($this->label))$this->label = $this->name;
        $this->name = strtolower($this->getDBName($this->name));
        $vardef = array();
        $vardef['name']=$this->name;
        if(empty($this->vname))$this->vname = 'LBL_' . strtoupper($this->name);
        $vardef['vname'] = $this->addLabel();
        if(!empty($this->required))$vardef['required'] = $this->required;
        if(empty($this->reportable))$vardef['reportable'] = false;
        if(!empty($this->comment))$vardef['comment'] = $this->comment;
        //Hack:Allow for changing the individual textboxes html size setting
        if(!empty($this->size))$vardef['size'] = $this->size;
        //Hack:Allow for changing the individual textboxes html size setting
        if($this->default !== 'MSI1')$vardef['default'] = $this->default;
        switch($this->type){
            case 'date':
            case 'datetime':

14.    /modules/ModuleBuilder/MB/MBPackage.php

$result=$db->query("SELECT *  FROM fields_meta_data where custom_module='$value'");
                    while($row = $db->fetchByAssoc($result)){
                        $name = $row['id'];
                        foreach($row as $col=>$res){
                            switch ($col) {
                                case 'custom_module':
                                    $installdefs['custom_fields'][$name]['module'] = $res;
                                    break;
                                case 'required':
                                    $installdefs['custom_fields'][$name]['require_option'] = $res;
                                    break;
                                case 'vname':
                                    $installdefs['custom_fields'][$name]['label'] = $res;
                                    break;
                            //Hack:Allow for changing the individual textboxes html size setting
                                case 'size':
                                    $installdefs['custom_fields'][$name]['size'] = $res;
                                    break;                                    
                            //Hack:Allow for changing the individual textboxes html size setting                        
                                case 'required':
                                    $installdefs['custom_fields'][$name]['require_option'] = $res;
                                    break;
                                case 'massupdate':
                                    $installdefs['custom_fields'][$name]['mass_update'] = $res;
                                    break;

15.    /modules/ModuleBuilder/views/view.modulefield.php

if(empty($vardef['name'])){
                if(!empty($_REQUEST['type']))$vardef['type'] = $_REQUEST['type'];
                    $fv->ss->assign('hideLevel', 0);
            }else{
                if(!empty($module->mbvardefs->vardef['fields'][$vardef['name']])){
                    $fv->ss->assign('hideLevel', 1);
                }elseif(isset($vardef['custom_module'])){
                    $fv->ss->assign('hideLevel', 2);
                }else{
                    //Hack:Allow for changing the individual textboxes html size setting 
                    if($vardef['name'] == 'name'){
                        $action = 'saveField';
                         
                    } else {
                        $action = 'saveLabel';
                    }
                    //Hack:Allow for changing the individual textboxes html size setting 
                    $fv->ss->assign('hideLevel', 10); // tyoung bug 17350 - effectively mark template derived fields as readonly
                }
            }

16.    /modules/ModuleBuilder/views/view.modulefields.php

} else {
            require_once('modules/ModuleBuilder/MB/ModuleBuilder.php');
            $mb = new ModuleBuilder();
            $mb->getPackage($_REQUEST['view_package']);
            $package = $mb->packages[$_REQUEST['view_package']];

            $package->getModule($module_name);
            $this->module = $package->modules[$module_name];
            $this->loadPackageHelp($module_name);
            $this->module->getVardefs(true);
            $this->module->mbvardefs->vardefs['fields'] = array_reverse($this->module->mbvardefs->vardefs['fields'], true);
            foreach($this->module->mbvardefs->vardefs['fields'] as $k=>$v){
                if($k != $module_name)
                    $titleLBL[$k]=translate("LBL_".strtoupper($k),'ModuleBuilder');
                else{
                    $titleLBL[$k]=$k;
                }
            }
            
            //Hack:Allow for changing the individual textboxes html size setting 
            foreach($this->module->mbvardefs->vardefs['fields'][$module_name] as $k=>$v){
                foreach($titleLBL as $template){
                    if($template != $module_name){
                        $template = strtolower($template);
                        
                        //Ketty: Upgrade sugarbase to SugarCRM 5.5
                        $template = str_replace(' ', '_', $template);
                        //Ketty: Upgrade sugarbase to SugarCRM 5.5
                        
                        foreach($this->module->mbvardefs->vardefs['fields'][$template] as $key => $value){
                            if($k == $key && $key == 'name'){
                                unset($this->module->mbvardefs->vardefs['fields'][$template][$key] );
                            }
                        }
                    }                
                }                
            }
            //Hack:Allow for changing the individual textboxes html size setting 
            $this->module->mbvardefs->vardefs['fields'][$module_name] = $this->cullFields($this->module->mbvardefs->vardefs['fields'][$module_name]);
            if(file_exists($this->module->path. '/language/'.$GLOBALS['current_language'].'.lang.php')){
                include($this->module->path .'/language/'. $GLOBALS['current_language'].'.lang.php');
                $this->module->setModStrings($GLOBALS['current_language'],$mod_strings);
            }
            elseif(file_exists($this->module->path. '/language/en_us.lang.php')){
                include($this->module->path .'/language/en_us.lang.php');
                $this->module->setModStrings('en_us',$mod_strings);
            }
            $smarty->assign('title', $titleLBL);
            $smarty->assign('package', $package);
            $smarty->assign('module', $this->module);
            $smarty->assign('editLabelsMb','1'); //need to merge MB labels and studio labels. quick fix for now.

17.    /include/SugarFields/Fields/Base/EditView.tpl

{if strlen({{sugarvar key='value' string=true}}) <= 0}
{assign var="value" value={{sugarvar key='default_value' string=true}} }
{else}
{assign var="value" value={{sugarvar key='value' string=true}} }
{/if}
{* Hack:Allow for changing the individual textboxes html size setting *}  
<input type='text' name='{{sugarvar key='name'}}' id='{{sugarvar key='name'}}' 
{{if isset($displayParams.maxlength)}}maxlength='{{$displayParams.maxlength}}'
{{elseif isset($vardef.len)}}maxlength='{{$vardef.len}}'{{/if}} 
{{if !empty($displayParams.size)}}size='{{$displayParams.size}}'
{{elseif !empty($vardef.size)}}size='{{$vardef.size}}'{{/if}} 
value='{$value}' title='{{$vardef.help}}' tabindex='{{$tabindex}}' {{$displayParams.field}}>



Additional InfoYou need to execute sql statement to add column “size” in "fields_meta_data" table.
ALTER TABLE fields_meta_data  add column size varchar (255) NULL ;

Sunday, October 31, 2010

.NET ConditionalAttribute

The preface to this blog post is found here Windows Forms .NET handling unhandled exceptions. The short story being that I have a smart client / winforms application running on the clients machine in release mode. All unhandled exceptions caught are reported to a sharepoint list. However in debug mode on the developer machine we do not want to handle unhandled exceptions since this can be useful when debugging in Visual Studio.

Hence we need to differentiate between debug and release mode.

This have traditionally been done using the compiler directives as here below
static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
#if DEBUG
            // To be able to debug properly we do not want to steal the exceptions from VS IDE
            safeMain();
#else
            // In release mode we want to be exception safe
                        try
            {
                    safeMain();
            }
            catch (Exception exception)
            {
                    handleUnhandledExceptions(exception);
            }
#endif
        }

        /// <summary>
        /// safeMain is a try catch all for any possible unhandled exceptions
        /// </summary>
        static void safeMain()
        {
#if !DEBUG 
            // In release mode we want to be exception safe
            // CLR unhandled exceptions handler
            AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
            // Windows forms exceptions handler
            Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
#endif
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new MainForm());
        }
This however is both messy and can lead to mistakes as I will show later.

The .NET framework have a useful attribute class that makes arranging this code in a better way very easy. The ConditionalAttribute.

Lets look as some code
using System;
using System.Linq;
using System.Windows.Forms;
using System.Diagnostics;
using System.Reflection;

namespace ConditionalAttrb
{
 public partial class Form1 : Form
 {
  public Form1()
  {
   InitializeComponent();
  }

  private void button1_Click(object sender, EventArgs e)
  {
   Log();
  }

  [Conditional("DEBUG")]
  private void Log()
  {
   label1.Text = "Inside Log()";
  }

  private void Form1_Load(object sender, EventArgs e)
  {
   if (IsAssemblyDebugBuild(this.GetType().Assembly))
   {
    this.Text = "Conditional Attribute (Debug)";
   }
   else
   {
    this.Text = "Conditional Attribute (Release)";
   }
  }

  private bool IsAssemblyDebugBuild(Assembly assembly)
  {
   return assembly.GetCustomAttributes(false).Any(x => (x as DebuggableAttribute) != null ? (x as DebuggableAttribute).IsJITTrackingEnabled : false);
  }
 }
}
The Form1_Load event handler method is only here used to set the correct title on the form so it is easy to see if you are in debug or release mode. It uses the IsAssemblyDebugBuild helper method for this.

The 2 only methods that are really relevant to the ConditionalAttribute class is the button1_Click event handler method and the Log() method.

You can see from the screenshots here below what happens when you run the application in debug / release mode and click the button.




This is more or less to be expected after all we only want the logging to happen when in debug mode.

So what is going on behind the scenes, lets have a look at the MSIL code for the Log() in debug and then release mode.

Debug:
.method private hidebysig instance void  Log() cil managed
{
  .custom instance void [mscorlib]System.Diagnostics.ConditionalAttribute::.ctor(string) = ( 01 00 05 44 45 42 55 47 00 00 )                   // ...DEBUG..
  // Code size       19 (0x13)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  ldfld      class [System.Windows.Forms]System.Windows.Forms.Label ConditionalAttrb.Form1::label1
  IL_0007:  ldstr      "Inside Log()"
  IL_000c:  callvirt   instance void [System.Windows.Forms]System.Windows.Forms.Control::set_Text(string)
  IL_0011:  nop
  IL_0012:  ret
} // end of method Form1::Log

Release:
.method private hidebysig instance void  Log() cil managed
{
  .custom instance void [mscorlib]System.Diagnostics.ConditionalAttribute::.ctor(string) = ( 01 00 05 44 45 42 55 47 00 00 )                   // ...DEBUG..
  // Code size       17 (0x11)
  .maxstack  8
  IL_0000:  ldarg.0
  IL_0001:  ldfld      class [System.Windows.Forms]System.Windows.Forms.Label ConditionalAttrb.Form1::label1
  IL_0006:  ldstr      "Inside Log()"
  IL_000b:  callvirt   instance void [System.Windows.Forms]System.Windows.Forms.Control::set_Text(string)
  IL_0010:  ret
} // end of method Form1::Log
If we study the MSIL code closely we see that the code is the same apart from 2 nop.

From this we can conclude that the method Log() is the same in debug and release mode. Now let us have a look at the calling button1_Click() method.

Debug:
.method private hidebysig instance void  button1_Click(object sender,
                                                       class [mscorlib]System.EventArgs e) cil managed
{
  // Code size       9 (0x9)
  .maxstack  8
  IL_0000:  nop
  IL_0001:  ldarg.0
  IL_0002:  call       instance void ConditionalAttrb.Form1::Log()
  IL_0007:  nop
  IL_0008:  ret
} // end of method Form1::button1_Click

Release:
.method private hidebysig instance void  button1_Click(object sender,
                                                       class [mscorlib]System.EventArgs e) cil managed
{
  // Code size       1 (0x1)
  .maxstack  8
  IL_0000:  ret
} // end of method Form1::button1_Click
If we look at this MSIL code we can see that it is the calling function that is modified not the Log() method. Since the code is JIT'ed the extra Log() method does not incur a performance overhead in release mode since the JIT compiler looks ahead and only JITs methods that will be called.

Ok so what is the benefit apart from that my code looks better?

Assuming that you are writing the following code
using System;
using System.Linq;
using System.Windows.Forms;
using System.Diagnostics;
using System.Reflection;

namespace ConditionalAttrb
{
 public partial class Form1 : Form
 {
  public Form1()
  {
   InitializeComponent();
  }

  private void button1_Click(object sender, EventArgs e)
  {
   string message = null;
#if DEBUG
   log = "Logging";
#endif 
   Log(message);
  }

  private void Log(string message)
  {
   if (message == null) throw new ArgumentNullException("message");
   label1.Text = message;
  }

  private void Form1_Load(object sender, EventArgs e)
  {
   if (IsAssemblyDebugBuild(this.GetType().Assembly))
   {
    this.Text = "Conditional Attribute (Debug)";
   }
   else
   {
    this.Text = "Conditional Attribute (Release)";
   }
  }

  private bool IsAssemblyDebugBuild(Assembly assembly)
  {
   return assembly.GetCustomAttributes(false).Any(x => (x as DebuggableAttribute) != null ? (x as DebuggableAttribute).IsJITTrackingEnabled : false);
  }
 }
}

This is a contrived example however it is not unusual for conditional code to become rather complex over the time of a project. And this code is actually introducing a fatal flaw in release mode which is not present in debug mode.



Ideally the code base should be the same in debug and release mode, and conditional compiler directives is in IMHO a bad idea when we have the ConditionalAttribute.

Saturday, October 30, 2010

Windows Forms .NET handling unhandled exceptions

This posting is a lead in to another post I am going to write about the ConditionalAttribute in the .NET framework. Since this posting has its own subject as well as setting the scene for the next posting I decided to keep them as 2 separate posts.

This code was written for a project some 3 years ago but is still relevant. The code was built based on a lot of investigations as well as online resources. Since it is some time ago the code was written I will not be able to mention all the sources, however the code was heavily inspired by 


Goals: Handle any unhandled exceptions in a windows forms application, log them to an arbitrary log location (Webservice, Sharepoint list or similar) as well as gracefully exit the windows application after encountering an unhandled exception.

First lets take a look at the Program.cs file that is containing the static Main() method.

using System;
using System.Windows.Forms;
using System.Threading;

namespace WinFormsUnhandledException
{
 static class Program
 {
  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main()
  {
#if DEBUG
   // To be able to debug properly we do not want to steal the exceptions from VS IDE
   safeMain();
#else
   // In release mode we want to be exception safe
   try
   {
     safeMain();
   }
   catch (Exception exception)
   {
     handleUnhandledExceptions(exception);
   }
#endif
  }

  /// <summary>
  /// safeMain is a try catch all for any possible unhandled exceptions
  /// </summary>
  static void safeMain()
  {
#if !DEBUG 
   // In release mode we want to be exception safe
   // CLR unhandled exceptions handler
   AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
   // Windows forms exceptions handler
   Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
#endif
   Application.EnableVisualStyles();
   Application.SetCompatibleTextRenderingDefault(false);
   Application.Run(new MainForm());
  }

  /// <summary>
  /// Handles any unhandled windows exceptions. The applicatin can continue running
  /// after this.
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
  {
   handleUnhandledExceptions(e.Exception);
  }

  /// <summary>
  /// Handles any unhandles CLR exceptions. Note that in .NET 2.0 unless you set
  /// <legacyUnhandledExceptionPolicy enabled="1"/> in the app.config file your
  /// application will exit no matter what you do here anyway. The purpose of this
  /// eventhandler is not forcing the application to continue but rather logging 
  /// the exception and enforcing a clean exit where all the finalizers are run 
  /// (releasing the resources they hold)
  /// </summary>
  /// <param name="sender"></param>
  /// <param name="e"></param>
  static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
  {
   handleUnhandledExceptions(e.ExceptionObject);

   // Prepare cooperative async shutdown from another thread
   Thread t = new Thread
   (
    delegate()
    {
     Environment.Exit(1);
    }
   );
   t.Start();
   t.Join();
  }


  /// <summary>
  /// Reports the unhandled exceptions
  /// </summary>
  /// <param name="exception"></param>
  static void handleUnhandledExceptions(object exception)
  {
   Exception e = exception as Exception;
   if (e != null)
   {
    ExceptionHelper.ReportException(e, new SpsSoftwareBugReporter(), new ReporterConfiguration("http://MYSHAREPOINTSERVER/SITE1/SITE2/_vti_bin/Lists.asmx"), string.Empty, true);
   }
  }
 }
}

There are several relevant points in this way to start a windows forms application and the comments in the code explains pretty well what is going on. Please note how cluttered the compiler directives for DEBUG ... is. These are the reason for my upcoming post on the ConditionalAttribute.

Basically this code in the Program.cs allows us to do the following in the main form.




MainForm.cs

using System;
using System.Windows.Forms;
using System.Threading;

namespace WinFormsUnhandledException
{
 public partial class MainForm : Form
 {
  public MainForm()
  {
   InitializeComponent();
  }

  private void button1_Click(object sender, EventArgs e)
  {
   throw new ArgumentNullException("something");
  }

  private void button2_Click(object sender, EventArgs e)
  {
   Thread.CurrentThread.Abort();
  }
 }
}

The Thread.CurrentThread.Abort(); line was specifically put there to show that this specific exception is a little bit different than the rest. This will in the current version of the code posted here show the exception dialog more than 1 time and potentially quite a few.  I will not go deep into Thread.Abort() now, but in short - don't use it. There are many articles about this on the net but a good reference I like is

Aborting Threads


Clicking on the 2 buttons above will show the following forms.




How do we get this form to appear and where do we get the information from? This is all happening in the ExceptionHelper class.


using System;
using System.Security;
using System.Reflection;
using System.Text;
using System.Diagnostics;
using System.Windows.Forms;

namespace WinFormsUnhandledException
{
 public class ExceptionHelper
 {
  /// <summary>
  /// Reports an exception a central place. This would most likely
  /// find a use in Windows smart clients in the 
  /// AppDomain.CurrentDomain.UnhandledException handler and the
  /// Application.ThreadException handler
  /// </summary>
  /// <param name="exception">The exception</param>
  /// <param name="reporter">The logic that takes care of actually reporting the error to the place you want it</param>
  /// <param name="reporterConfiguration">Storage configuration for the reporter</param>
  /// <param name="userComments">User comments about the error</param>
  /// <param name="showForm">True - show error form and ask user to accept report.
  /// False - dont show form just report</param>
  public static void ReportException(Exception exception, IReporter<UnhandledExceptionReport> reporter, ReporterConfiguration reporterConfiguration, string userComments, bool showForm)
  {
   StringBuilder sb = new StringBuilder();
   Assembly a = Assembly.GetEntryAssembly();
   UnhandledExceptionReport unhandledExceptionReport = new UnhandledExceptionReport();
   unhandledExceptionReport.Application = a.FullName;

   AssemblyName[] references = a.GetReferencedAssemblies();
   foreach (AssemblyName reference in references)
   {
    sb.Append(reference.FullName);
    sb.Append(Environment.NewLine);
   }

   unhandledExceptionReport.Assemblies = sb.ToString();
   unhandledExceptionReport.Date = DateTime.Now.ToString();
   unhandledExceptionReport.Exceptions = exception.ToString();
   unhandledExceptionReport.FileAttachments = new string[] { GetSystemInfo() };
   unhandledExceptionReport.UserComments = userComments;

   if (showForm)
   {
    UnhandledExceptionReportForm form = new UnhandledExceptionReportForm(unhandledExceptionReport);
    if (form.ShowDialog() == DialogResult.OK)
    {
     reporter.Submit(unhandledExceptionReport);
    }
   }
   else
   {
    reporter.Submit(unhandledExceptionReport);
   }
  }

  
  /// <summary>
  /// Calls the MSINFO32.exe and generates a nfo file. The path of the 
  /// generated file is returned.
  /// </summary>
  /// <returns>Path of generated msinfo32 report file</returns>
  public static string GetSystemInfo()
  {
   try
   {
    // retrieve the path to MSINFO32.EXE
    Microsoft.Win32.RegistryKey key;
    key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Shared Tools\MSInfo");
    if (key != null)
    {
     string exeFile = key.GetValue("Path", "").ToString();
     key.Close();

     if (exeFile.Length != 0)
     {
      ProcessStartInfo processStartInfo = new ProcessStartInfo();
      processStartInfo.FileName = exeFile;
      processStartInfo.Arguments = "/nfo " + Environment.GetEnvironmentVariable("TEMP") + "\\sysinfo /categories +systemsummary-Resources-Components-SWEnv-InternetSettings-Apps11+SWEnvEnvVars+SWEnvRunningTasks+SWEnvServices";
      Process process = Process.Start(processStartInfo);
      process.WaitForExit();
     }
    }
    return Environment.GetEnvironmentVariable("TEMP") + "\\sysinfo.nfo";
   }
   catch (SecurityException)
   {
    // mostlikely due to not having the correct permissions
    // to access the registry, ignore this exception
    return null;
   }
  }
 }
}


This class gathers the information about the exception as well as a system information dump and shows the Unhandled Exception Form where the user can enter some information to explain what they did to provoke the exception.

I am using the following support classes
using System;

namespace WinFormsUnhandledException
{
 /// 
 /// 
 /// 
 public class UnhandledExceptionReport
 {
  public UnhandledExceptionReport()
  { 
   Title = GetType().Name;
  }
  public string Title {get; set;}
  public string Application {get; set;}
  public string Date {get; set;}
  public string UserComments {get; set;}
  public string Exceptions {get; set;}
  public string Assemblies {get; set;}
  public string[] FileAttachments { get; set; }
 }
}

Here is the code for the UnhandledExceptionReportForm

using System;
using System.Windows.Forms;
using System.Diagnostics;

namespace WinFormsUnhandledException
{
 public partial class UnhandledExceptionReportForm : Form
 {
  private UnhandledExceptionReport m_unhandledExceptionReport;

  public UnhandledExceptionReportForm()
  {
   InitializeComponent();
  }
  public UnhandledExceptionReportForm(UnhandledExceptionReport unhandledExceptionReport) : this()
  {
   this.m_unhandledExceptionReport = unhandledExceptionReport;
   if (m_unhandledExceptionReport != null && m_unhandledExceptionReport.UserComments == string.Empty)
   {
    m_unhandledExceptionReport.UserComments = "Please write a description of what you were doing when the error occured";
   }
   
   titleTextBox.Text = m_unhandledExceptionReport.Title;
   applicationTextBox.Text = m_unhandledExceptionReport.Application;
   dateTextBox.Text = m_unhandledExceptionReport.Date;
   commentsTextBox.DataBindings.Add("Text", m_unhandledExceptionReport, "UserComments");
   exceptionsTextBox.Text = m_unhandledExceptionReport.Exceptions;
   assembliesTextBox.Text = m_unhandledExceptionReport.Assemblies;
   attachmentLinkLabel.Text = m_unhandledExceptionReport.FileAttachments[0];
  }

  private void reportButton_Click(object sender, EventArgs e)
  {
   Hide();
   DialogResult = DialogResult.OK;
  }

  private void cancelButton_Click(object sender, EventArgs e)
  {
   Hide();
   DialogResult = DialogResult.Cancel;
  }

  private void attachmentLinkLabel_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
  {
   Process.Start(m_unhandledExceptionReport.FileAttachments[0]);
  }

 }
}

Finally we have the interface for the class doing the actual error reporting

using System;

namespace WinFormsUnhandledException
{
 
 public interface IReporter<T>
 {
  /// 
  /// Reports some information to a specific location
  /// 
  /// The type to be reported on  
  /// The configuration the reporter need  
  void Submit(T report, ReporterConfiguration config);
 }
}

and here is a IReported implementation that is reporting unhandled exceptions to a Sharepoint list.

using System;
using System.Text;
using System.Reflection;
using System.Net;
using System.Xml;
using System.IO;
using System.Xml.XPath;

using System.Security;

namespace WinFormsUnhandledException
{
 public class SpsSoftwareBugReporter : IReporter<UnhandledExceptionReport>
 {
  #region IReporter<SoftwareBug> Members

  void IReporter<UnhandledExceptionReport>.Submit(UnhandledExceptionReport report, ReporterConfiguration config)
  {
   ListsService.Lists service = new ListsService.Lists();
   service.Url = config.ConnectionString;
   service.Credentials = CredentialCache.DefaultCredentials;

   XmlNode list = service.GetList("Bugs");
   string listName = list.Attributes["ID"].Value;

   StringBuilder sb = new StringBuilder();
   sb.Append("<Method ID=\"1\" Cmd=\"New\">");
   sb.Append("<Field Name=\"ID\">New</Field>");
   PropertyInfo[] properties = report.GetType().GetProperties();

   foreach (PropertyInfo propertyInfo in properties)
   {
    if (propertyInfo.Name != "FileAttachments")
    {
     sb.Append("<Field Name=\"");
     sb.Append(propertyInfo.Name);
     sb.Append("\">");
     sb.Append(SecurityElement.Escape(propertyInfo.GetValue(report, null).ToString()));
     sb.Append("</Field>");
    }
   }
   sb.Append("</Method>");

   XmlDocument doc = new XmlDocument();
   XmlElement newBugs = doc.CreateElement("Batch");
   newBugs.SetAttribute("OnError", "Continue");
   newBugs.SetAttribute("ListVersion", list.Attributes["Version"].Value);
   
   
   
   newBugs.InnerXml =  sb.ToString();
   // Add bug report
   XmlNode result = service.UpdateListItems(listName, newBugs);

   // Add item attachments
   XPathNavigator navigator = result.CreateNavigator();
   XmlNamespaceManager manager = new XmlNamespaceManager(navigator.NameTable);
   manager.AddNamespace("def", "http://schemas.microsoft.com/sharepoint/soap/");
   manager.AddNamespace("z", "#RowsetSchema");

   XPathNavigator node = navigator.SelectSingleNode("//def:ErrorCode", manager);
   if (node.InnerXml == "0x00000000")
   {
    XmlNodeList nodes = result.SelectNodes("//z:row", manager);
    
    foreach (string  attachment in report.FileAttachments)
    {
     if (attachment != null || attachment != string.Empty)
     {
      using (FileStream stream = File.OpenRead(attachment))
      {
       try
       {
        service.AddAttachment(listName, nodes[0].Attributes["ows_ID"].Value, Path.GetFileName(attachment), StreamHelper.ReadFully(stream));
       }
       catch (Exception exception)
       {
        Console.WriteLine(exception.ToString());
       }
      }
     }
    }
   }
  }
  #endregion
 }
}

Friday, October 15, 2010

SugarCrm 5.5.2 Editview: Fieldlevel localizable tooltip in Module Builder

Sugar and module builder supports field specific help that will be shown as tooltip of a control in the different views. There is unfortunately no localization support for this in sugar (meaning that the tooltips will always be in one language).
This posting and hack adds that localization support of vardefs field help text.

The way this works is:

1. Create or edit a field in Module Builder. (You will notice a new help text & help label field.)
2. The default language help text needs to be entered in help text. (There is no need to modify help label name.)



3. Save the field.
4. Go to labels localization screen, change the desired language, find the field help label and enter localized value then save the label. Put the field in one of the layouts. Then deploy package.




5. Based on login language, help tool tip value will be shown from language file.




This customization is nonupgrade safe.
The following files have been modified

1. include/EditView/EditView2.php
a. Following block added in function display to assign localized help value to editview.
//HACK : -- Add localization to vardef field_help    
foreach($this->fieldDefs as $key => $val){
 $tempk = ''; $tempv='';
 if(isset($this->fieldDefs[$key]['help'])){
  $tempk = $this->fieldDefs[$key]['help'];
 }else{
  $tempk = "LBL_".mb_strtoupper($this->fieldDefs[$key]['name'])."_HELP";
 }
 $tempv = translate($tempk, $this->module);         
 if($tempv!=$tempk){
 //HACK :-- Translating help fields
  //$this->fieldDefs[$key]['help'] = $tempv;
  if($this->fieldDefs[$key]['type']=='radioenum'){
   $this->fieldDefs[$key]['help'] = $tempk;
  }else{
   $this->fieldDefs[$key]['help'] = "{php}echo smarty_function_sugar_translate(array('label' => '$tempk' ,'module' => '$this->module'), \$this);{/php}";
  } 
 //HACK : -- Add localization to vardef field_help
 }else{
  $this->fieldDefs[$key]['help'] ='';  
 }
 if(isset($this->fieldDefs[$key]['validation']['functionName']) && !empty($this->fieldDefs[$key]['validation']['functionName'])){
  if(isset($this->fieldDefs[$key]['customjs'])){
     $tempscript.='{literal}<script type="text/javascript" language="Javascript">'.html_entity_decode($this->fieldDefs[$key]['customjs'],ENT_QUOTES).'</script>{/literal}';
  }
  if(isset($this->fieldDefs[$key]['validation'])&& is_array($this->fieldDefs[$key]['validation'])!=true){
     $this->fieldDefs[$key]['validation'] = unserialize(html_entity_decode($this->fieldDefs[$key]['validation']));
  }
 }
}
 $this->th->ss->assign('scriptBlocks', $tempscript);
//HACK :


2. include/SearchForm/SearchForm2.php
a. Following block added in function display to assign localized help value to editview.
//HACK : -- Add localization to vardef field_help  
foreach($this->fieldDefs as $key => $val){
 $tempk = ''; $tempv='';
 if(isset($this->fieldDefs[$key]['help'])){
  $tempk = $this->fieldDefs[$key]['help'];
 }else{
  $tempk = "LBL_".mb_strtoupper($this->fieldDefs[$key]['name'])."_HELP";
 }
 $tempv = translate($tempk, $this->module);         
 if($tempv!=$tempk){
 //HACK :-- Translating help fields
    //$this->fieldDefs[$key]['help'] = $tempv;
  if($this->fieldDefs[$key]['type']=='radioenum'){
   $this->fieldDefs[$key]['help'] = $tempk;
  }else{
   $this->fieldDefs[$key]['help'] = "{php}echo smarty_function_sugar_translate(array('label' => '$tempk' ,'module' => '$this->module'), \$this);{/php}";
  } 
 //HACK : -- Add localization to vardef field_help
 }else{
  $this->fieldDefs[$key]['help'] ='';  
 }
}
//HACK :


3. modules/DynamicFields/DynamicField.php
a. In function addFieldObject, value of $fmd->help set from $field->help_label.
//HACK : -- Add localization to vardef field_help
$fmd->help = $field->help_label;
//HACK : -- Add localization to vardef field_help


4. modules/DynamicFields/templates/Fields/Forms/coreTop.tpl
a. Template modified for help label & its automatic assignment of value based on its field name.
<table width="100%">
<tr>
 <td class='mbLBL' width='30%' >{$MOD.COLUMN_TITLE_NAME}:</td>
 <td>
 {if $hideLevel == 0}
  {* HACK:  -- Add localization to vardef field_help 
   Added javascript statement for automatic help label assignment *}
  <input id="field_name_id" maxlength=30 type="text" name="name" value="{$vardef.name}" onchange="document.getElementById('label_key_id').value = 'LBL_'+document.getElementById('field_name_id').value.toUpperCase();
  document.getElementById('label_value_id').value = document.getElementById('field_name_id').value.replace(/_/,' ');
  document.getElementById('field_name_id').value = document.getElementById('field_name_id').value.toLowerCase();  
  document.getElementById('help_key_id').value = 'LBL_'+(document.getElementById('field_name_id').value.toUpperCase())+'_HELP';" />
  {* HACK:  *}
 {else}
  {* HACK:  -- Add localization to vardef field_help 
   Added javascript statement for automatic help label assignment *}
  <input id= "field_name_id" maxlength=30 type="hidden" name="name" value="{$vardef.name}" 
    onchange="document.getElementById('label_key_id').value = 'LBL_'+document.getElementById('field_name_id').value.toUpperCase();
  document.getElementById('label_value_id').value = document.getElementById('field_name_id').value.replace(/_/,' ');
  document.getElementById('field_name_id').value = document.getElementById('field_name_id').value.toLowerCase();
  document.getElementById('help_key_id').value = 'LBL_'+(document.getElementById('field_name_id').value.toUpperCase())+'_HELP';"/>{$vardef.name}{/if}
  {* HACK: *}
  <script>
  addToValidate('popup_form', 'name', 'DBName', true,'{$MOD.COLUMN_TITLE_NAME} [a-zA-Z_]' );
  addToValidateIsInArray('popup_form', 'name', 'in_array', true,'{$MOD.ERR_RESERVED_FIELD_NAME}', '{$field_name_exceptions}', 'u==');
  </script>
 </td>
</tr>
<tr>
 <td class='mbLBL'>{$MOD.COLUMN_TITLE_DISPLAY_LABEL}:</td>
 <td>
  <input id="label_value_id" type="text" name="labelValue" value="{$lbl_value}">
 </td>
</tr>
<tr>
 <td class='mbLBL'>{$MOD.COLUMN_TITLE_LABEL}:</td>
 <td>
 {if $hideLevel < 5}
  <input id ="label_key_id" type="text" name="label" value="{$vardef.vname}">
 {else}
  <input id ="label_key_id" type="hidden" name="label" value="{$vardef.vname}">{$vardef.vname}
 {/if}
 </td>
</tr>
{* HACK:  -- Add localization to vardef field_help ,Course module Accounting type does not work *}
<tr>
 <td class='mbLBL'>{$MOD.COLUMN_TITLE_HELP_TEXT}:</td>
 <td>
  <input type="text" id="help_value_id" name="help" value="{$vardef.help}" onchange="if(document.getElementById('help_key_id').value == '') document.getElementById('help_key_id').value='LBL_'+(document.getElementById('field_name_id').value.toUpperCase())+'_HELP';">
 </td>
</tr>

<tr>
 <td class='mbLBL'>{$MOD.COLUMN_TITLE_HELP_LABEL}:</td>
 <td>{if $hideLevel < 5 }
  <input type="text" id="help_key_id" name="help_label" value="{$vardef.help_label}">
  {else}
  <input type="hidden" id="help_key_id" name="help_label" value="{$vardef.help_label}">{$vardef.help_label}
  {/if}
 </td>
</tr>
{* HACK: *}
<tr>
    <td class='mbLBL'>{$MOD.COLUMN_TITLE_COMMENT_TEXT}:</td><td>{if $hideLevel < 5 }<input type="text" name="comments" value="{$vardef.comments}">{else}<input type="hidden" name="comment" value="{$vardef.comment}">{$vardef.comment}{/if}
    </td>
</tr>


5. modules/DynamicFields/templates/Fields/TemplateField.php
a. New class variable added
//HACK : -- Add localization to vardef field_help
// for initialisation of bean value.
var $help_label ='';
//HACK : -- Add localization to vardef field_help

b. New key value pair added to array for new vardef property help_label
var $vardef_map = array(
 'name'=>'name',
 'label'=>'vname',
 // bug 15801 - need to ALWAYS keep default and default_value consistent as some methods/classes use one, some use another...
 'default_value'=>'default',
 'default'=>'default_value',
 //'default_value'=>'default_value',
 //'default'=>'default_value',
 'len'=>'len',
 'required'=>'required',
 'type'=>'type',
 'audited'=>'audited',
 'massupdate'=>'massupdate',
 'options'=>'ext1',
 'help'=>'help',
 'comments'=>'comment',
 'importable'=>'importable',
 'duplicate_merge'=>'duplicate_merge',
 //'duplicate_merge_dom_value'=>'duplicate_merge', //bug #14897
 'reportable' => 'reportable',
 'min'=>'ext1',
 'max'=>'ext2',
 'ext2'=>'ext2',
 'ext4'=>'ext4',
 //'disable_num_format'=>'ext3',
 'ext3'=>'ext3',
 'label_value'=>'label_value',

 //HACK :  -- Add localization to vardef field_help
 //used for vardef mapping.
 'help_label'=>'help_label',
 //HACK :  
);


6. modules/EditCustomFields/FieldsMetaData.php
a. In class FieldsMetaData 2 new class variable added.
//HACK : -- Add localization to vardef field_help
var $help;
var $help_label;
//HACK : -- Add localization to vardef field_help

b. 2 New key value pair added to array for new vardef property .
var $column_fields = array(
 'id',
 'name',
 'vname',
 'custom_module',
 'type',
 'len',
 'required',
 'default_value',
 'deleted',
 'ext1',
 'ext2',
 'ext3',
 //HACK : -- Add localization to vardef field_help
 'help',
 'help_label', 
 //HACK : 
 'audited',
 'massupdate',
 'duplicate_merge',
 'reportable',
);


7. modules/EditCustomFields/language/en_us.lang.php
a. Help label
// HACK : -- Add localization to vardef field_help
// label for the new help label 
$mod_strings['COLUMN_TITLE_HELP_LABEL'] = 'Help Label';
// HACK : -- Add localization to vardef field_help


8. modules/EditCustomFields/vardefs.php
a. New field added for help_label.
// HACK : -- Add localization to vardef field_help 
// Added for adding new field help label in module builder. 
$dictionary['FieldsMetaData']['fields']['help_label']=array('name' =>'help_label' ,'type' =>'varchar','vname'=>'COLUMN_TITLE_LABEL', 'len'=>'255');
// HACK : -- Add localization to vardef field_help


9. modules/ModuleBuilder/controller.php
a. function action_SaveLabel modified to set value of help_label.
// HACK : -- Add localization to vardef field_help
 if (isset($_REQUEST['help_label']) && isset ($_REQUEST['help' ])){
 if(empty($_REQUEST['help_label'])){
  $_REQUEST['help_label']="LBL_".$_REQUEST['name']."_HELP";
 }
 $_REQUEST [ "label_" . $_REQUEST['help_label'] ] = $_REQUEST [ 'help' ] ;
 } 
// HACK : -- Add localization to vardef field_help

b. function action_SaveField is modified to set help_label value in vardefs before saving mbvardefs & value of label in language file of current language before saving modue.
//HACK : -- Add localization to vardef field_help
if(isset($_REQUEST['help_label']) && empty($_REQUEST['help_label']))
  $_REQUEST['help_label']="LBL_".mb_strtoupper($_REQUEST['name'])."_HELP";         

$module->mbvardefs->vardef['fields'][$_REQUEST['name']]['help']=$_REQUEST['help_label'];
//HACK :
$module->mbvardefs->save () ;
// get the module again to refresh the labels we might have saved with the $field->save (e.g., for address fields)
$module = & $mb->getPackageModule ( $_REQUEST [ 'view_package' ], $_REQUEST [ 'view_module' ] ) ;
if (isset ( $_REQUEST [ 'label' ] ) && isset ( $_REQUEST [ 'labelValue' ] ))
 $module->setLabel ( $GLOBALS [ 'current_language' ], $_REQUEST [ 'label' ], $_REQUEST [ 'labelValue' ] ) ;
//HACK : -- Add localization to vardef field_help
//setting the help text and label for current language.            
if (isset($_REQUEST['help_label']) && isset ($_REQUEST['help']))
 $module->setLabel($GLOBALS['current_language'],$_REQUEST['help_label'],$_REQUEST['help']);         
//HACK :


10. modules/ModuleBuilder/views/view.modulefield.php
a. Code block added to set value fo $vardef[‘help’] and $vardef[‘help_label’]. Following code block is for studio code.
//HACK : -- Add localization to vardef field_help
$temp_var_help_translation='';
if(isset($vardef['help'])){
 if(!empty($vardef['help'])){
  $temp_var_help_translation=translate(mb_strtoupper($vardef['help']),$moduleName);
  if($temp_var_help_translation != mb_strtoupper($vardef['help'])){
   $vardef['help_label']=$vardef['help'];
   $vardef['help']=$temp_var_help_translation;
  }else{
   $vardef['help_label']=$vardef['help'];
   $vardef['help']='';      
  }
 }
}elseif(!empty($vardef['name'])){ 
 $tempkey="LBL_".mb_strtoupper($vardef['name'])."_HELP";
 $temp_var_help_translation = translate(mb_strtoupper($tempkey),$moduleName);
 if($temp_var_help_translation != mb_strtoupper($tempkey)){
  $vardef['help_label']=$tempkey;
  $vardef['help']=$temp_var_help_translation;
 }else{
  $vardef['help_label']=$tempkey;
  $vardef['help']='';          
 }
}
//HACK :

b. Code block added to set value fo $vardef[‘help’] and $vardef[‘help_label’]. Following code block is for module builder code.
//HACK : -- Add localization to vardef field_help
$temp_var_help_translation='';
if(isset($vardef['help'])){ 
 if(!empty($vardef['help'])){
  //HACK:Upgrade sugarbase to SugarCRM 5.5
  $temp_var_help_translation=$module->getLabel($GLOBALS['current_language'],$vardef['help']);
  //HACK:Upgrade sugarbase to SugarCRM 5.5
  if(!empty($temp_var_help_translation)){
   $vardef['help_label']=$vardef['help'];
   $vardef['help']=$temp_var_help_translation;
  }else{
   $vardef['help_label']=$vardef['help'];
   $vardef['help']='';      
  }
 }
}elseif(!empty($vardef['name'])){ 
 $tempkey="LBL_".mb_strtoupper($vardef['name'])."_HELP";
 //HACK:Upgrade sugarbase to SugarCRM 5.5
 $temp_var_help_translation=$module->getLabel($GLOBALS['current_language'],$tempkey);
 //HACK:Upgrade sugarbase to SugarCRM 5.5
 if(!empty($temp_var_help_translation)){
  $vardef['help_label']=$tempkey;
  $vardef['help']=$temp_var_help_translation;
 }else{
  $vardef['help_label']=$tempkey;
  $vardef['help']='';          
 }
}
//HACK :


11. Add new field in fields_meta_data table by running following query.
ALTER TABLE fields_meta_data 
add column help_label varchar (255) Null

Changes made in language file & vardefs files are not upgrade safe. While building package move it to upgrade-safe files.

And finally the SVN diff containing the changes


Index: include/EditView/EditView2.php
===================================================================
--- include/EditView/EditView2.php (revision 1)
+++ include/EditView/EditView2.php (working copy)
@@ -447,6 +447,38 @@
         $this->th->ss->assign('offset', $this->offset + 1);
         $this->th->ss->assign('APP', $app_strings);
         $this->th->ss->assign('MOD', $mod_strings);
+  //HACK : -- Add localization to vardef field_help    
+        foreach($this->fieldDefs as $key => $val){
+            $tempk = ''; $tempv='';
+            if(isset($this->fieldDefs[$key]['help'])){
+             $tempk = $this->fieldDefs[$key]['help'];
+            }else{
+             $tempk = "LBL_".mb_strtoupper($this->fieldDefs[$key]['name'])."_HELP";
+            }
+         $tempv = translate($tempk, $this->module);         
+            if($tempv!=$tempk){
+   //HACK :-- Translating help fields
+                //$this->fieldDefs[$key]['help'] = $tempv;
+    if($this->fieldDefs[$key]['type']=='radioenum'){
+                 $this->fieldDefs[$key]['help'] = $tempk;
+                }else{
+     $this->fieldDefs[$key]['help'] = "{php}echo smarty_function_sugar_translate(array('label' => '$tempk' ,'module' => '$this->module'), \$this);{/php}";
+    } 
+   //HACK : 
+            }else{
+             $this->fieldDefs[$key]['help'] ='';  
+            }
+            if(isset($this->fieldDefs[$key]['validation']['functionName']) && !empty($this->fieldDefs[$key]['validation']['functionName'])){
+    if(isset($this->fieldDefs[$key]['customjs'])){
+                $tempscript.='{literal}<script type="text/javascript" language="Javascript">'.html_entity_decode($this->fieldDefs[$key]['customjs'],ENT_QUOTES).'</script>{/literal}';
+             }
+             if(isset($this->fieldDefs[$key]['validation'])&& is_array($this->fieldDefs[$key]['validation'])!=true){
+                $this->fieldDefs[$key]['validation'] = unserialize(html_entity_decode($this->fieldDefs[$key]['validation']));
+             }
+   }
+        }
+         $this->th->ss->assign('scriptBlocks', $tempscript);
+        //HACK : 
         $this->th->ss->assign('fields', $this->fieldDefs);
         $this->th->ss->assign('sectionPanels', $this->sectionPanels);
         $this->th->ss->assign('returnModule', $this->returnModule);
Index: include/SearchForm/SearchForm2.php
===================================================================
--- include/SearchForm/SearchForm2.php (revision 1)
+++ include/SearchForm/SearchForm2.php (working copy)
@@ -161,7 +161,29 @@
   if($this->nbTabs>1){
       $this->th->ss->assign('TABS', $this->_displayTabs($this->module . '|' . $this->displayView));
   }
-
+      //HACK : -- Add localization to vardef field_help  
+      foreach($this->fieldDefs as $key => $val){
+            $tempk = ''; $tempv='';
+            if(isset($this->fieldDefs[$key]['help'])){
+             $tempk = $this->fieldDefs[$key]['help'];
+            }else{
+             $tempk = "LBL_".mb_strtoupper($this->fieldDefs[$key]['name'])."_HELP";
+            }
+         $tempv = translate($tempk, $this->module);         
+            if($tempv!=$tempk){
+   //HACK :-- Translating help fields
+               //$this->fieldDefs[$key]['help'] = $tempv;
+       if($this->fieldDefs[$key]['type']=='radioenum'){
+                 $this->fieldDefs[$key]['help'] = $tempk;
+                }else{
+     $this->fieldDefs[$key]['help'] = "{php}echo smarty_function_sugar_translate(array('label' => '$tempk' ,'module' => '$this->module'), \$this);{/php}";
+    } 
+   //HACK : 
+            }else{
+             $this->fieldDefs[$key]['help'] ='';  
+            }
+        }
+        //HACK :
   $this->th->ss->assign('fields', $this->fieldDefs);
   $this->th->ss->assign('customFields', $this->customFieldDefs);
   $this->th->ss->assign('formData', $this->formData);
Index: modules/DynamicFields/DynamicField.php
===================================================================
--- modules/DynamicFields/DynamicField.php (revision 1)
+++ modules/DynamicFields/DynamicField.php (working copy)
@@ -507,7 +507,9 @@
         $fmd->name = $db_name;
         $fmd->vname = $label;
         $fmd->type = $field->type;
-        $fmd->help = $field->help;
+        //HACK : -- Add localization to vardef field_help
+        $fmd->help = $field->help_label;
+        //HACK :
         if (!empty($field->len))
             $fmd->len = $field->len; // tyoung bug 15407 - was being set to $field->size so changes weren't being saved
         $fmd->required = ($field->required ? 1 : 0);
Index: modules/DynamicFields/templates/Fields/Forms/coreTop.tpl
===================================================================
--- modules/DynamicFields/templates/Fields/Forms/coreTop.tpl (revision 1)
+++ modules/DynamicFields/templates/Fields/Forms/coreTop.tpl (working copy)
@@ -44,19 +44,22 @@
  <td class='mbLBL' width='30%' >{$MOD.COLUMN_TITLE_NAME}:</td>
  <td>
  {if $hideLevel == 0}
-  <input id="field_name_id" maxlength=30 type="text" name="name" value="{$vardef.name}" 
-    onchange="
-  document.getElementById('label_key_id').value = 'LBL_'+document.getElementById('field_name_id').value.toUpperCase();
+  {* HACK:  -- Add localization to vardef field_help 
+   Added javascript statement for automatic help label assignment *}
+  <input id="field_name_id" maxlength=30 type="text" name="name" value="{$vardef.name}" onchange="document.getElementById('label_key_id').value = 'LBL_'+document.getElementById('field_name_id').value.toUpperCase();
   document.getElementById('label_value_id').value = document.getElementById('field_name_id').value.replace(/_/,' ');
-  document.getElementById('field_name_id').value = document.getElementById('field_name_id').value.toLowerCase();" />
+  document.getElementById('field_name_id').value = document.getElementById('field_name_id').value.toLowerCase();  
+  document.getElementById('help_key_id').value = 'LBL_'+(document.getElementById('field_name_id').value.toUpperCase())+'_HELP';" />
+  {* HACK:  *}
  {else}
+  {* HACK:  -- Add localization to vardef field_help 
+   Added javascript statement for automatic help label assignment *}
   <input id= "field_name_id" maxlength=30 type="hidden" name="name" value="{$vardef.name}" 
-    onchange="
-  document.getElementById('label_key_id').value = 'LBL_'+document.getElementById('field_name_id').value.toUpperCase();
+    onchange="document.getElementById('label_key_id').value = 'LBL_'+document.getElementById('field_name_id').value.toUpperCase();
   document.getElementById('label_value_id').value = document.getElementById('field_name_id').value.replace(/_/,' ');
-  document.getElementById('field_name_id').value = document.getElementById('field_name_id').value.toLowerCase();"/>
-  {$vardef.name}
- {/if}
+  document.getElementById('field_name_id').value = document.getElementById('field_name_id').value.toLowerCase();
+  document.getElementById('help_key_id').value = 'LBL_'+(document.getElementById('field_name_id').value.toUpperCase())+'_HELP';"/>{$vardef.name}{/if}
+  {* HACK: *}
   <script>
   addToValidate('popup_form', 'name', 'DBName', true,'{$MOD.COLUMN_TITLE_NAME} [a-zA-Z_]' );
   addToValidateIsInArray('popup_form', 'name', 'in_array', true,'{$MOD.ERR_RESERVED_FIELD_NAME}', '{$field_name_exceptions}', 'u==');
@@ -79,11 +82,25 @@
  {/if}
  </td>
 </tr>
+{* HACK:  -- Add localization to vardef field_help ,Course module Accounting type does not work *}
 <tr>
- <td class='mbLBL'>{$MOD.COLUMN_TITLE_HELP_TEXT}:</td><td>{if $hideLevel < 5 }<input type="text" name="help" value="{$vardef.help}">{else}<input type="hidden" name="help" value="{$vardef.help}">{$vardef.help}{/if}
+ <td class='mbLBL'>{$MOD.COLUMN_TITLE_HELP_TEXT}:</td>
+ <td>
+  <input type="text" id="help_value_id" name="help" value="{$vardef.help}" onchange="if(document.getElementById('help_key_id').value == '') document.getElementById('help_key_id').value='LBL_'+(document.getElementById('field_name_id').value.toUpperCase())+'_HELP';">
  </td>
 </tr>
+
 <tr>
+ <td class='mbLBL'>{$MOD.COLUMN_TITLE_HELP_LABEL}:</td>
+ <td>{if $hideLevel < 5 }
+  <input type="text" id="help_key_id" name="help_label" value="{$vardef.help_label}">
+  {else}
+  <input type="hidden" id="help_key_id" name="help_label" value="{$vardef.help_label}">{$vardef.help_label}
+  {/if}
+ </td>
+</tr>
+{* HACK: *}
+<tr>
     <td class='mbLBL'>{$MOD.COLUMN_TITLE_COMMENT_TEXT}:</td><td>{if $hideLevel < 5 }<input type="text" name="comments" value="{$vardef.comments}">{else}<input type="hidden" name="comment" value="{$vardef.comment}">{$vardef.comment}{/if}
     </td>
 </tr>
Index: modules/DynamicFields/templates/Fields/TemplateField.php
===================================================================
--- modules/DynamicFields/templates/Fields/TemplateField.php (revision 1)
+++ modules/DynamicFields/templates/Fields/TemplateField.php (working copy)
@@ -67,6 +67,10 @@
  var $reportable = false;
  var $label_value = '';
  var $help = '';
+ //HACK : -- Add localization to vardef field_help
+ // for initialisation of bean value.
+ var $help_label ='';
+ //HACK :
 
  var $vardef_map = array(
   'name'=>'name',
@@ -95,6 +99,11 @@
      //'disable_num_format'=>'ext3',
      'ext3'=>'ext3',
   'label_value'=>'label_value',
+
+  //HACK :  -- Add localization to vardef field_help
+  //used for vardef mapping.
+  'help_label'=>'help_label',
+  //HACK :  
  );
  /*
   HTML FUNCTIONS
Index: modules/EditCustomFields/FieldsMetaData.php
===================================================================
--- modules/EditCustomFields/FieldsMetaData.php (revision 1)
+++ modules/EditCustomFields/FieldsMetaData.php (working copy)
@@ -60,6 +60,10 @@
    var $ext1;
    var $ext2;
    var $ext3;
+ //HACK : -- Add localization to vardef field_help
+   var $help;
+   var $help_label;
+   //HACK :
  var $audited;
     var $duplicate_merge;
     var $reportable;
@@ -81,6 +85,10 @@
   'ext1',
   'ext2',
   'ext3',
+     //HACK : -- Add localization to vardef field_help
+  'help',
+  'help_label', 
+     //HACK : 
   'audited',
   'massupdate',
         'duplicate_merge',
Index: modules/EditCustomFields/language/en_us.lang.php
===================================================================
--- modules/EditCustomFields/language/en_us.lang.php (revision 1)
+++ modules/EditCustomFields/language/en_us.lang.php (working copy)
@@ -92,4 +92,9 @@
  'LBL_DEPENDENT_TRIGGER'=>'Trigger',
  'LBL_BTN_EDIT_VISIBILITY'=>'Edit Visibility',
 );
+
+// HACK : -- Add localization to vardef field_help
+// label for the new help label 
+$mod_strings['COLUMN_TITLE_HELP_LABEL'] = 'Help Label';
+// HACK :
 ?>
Index: modules/EditCustomFields/vardefs.php
===================================================================
--- modules/EditCustomFields/vardefs.php (revision 1)
+++ modules/EditCustomFields/vardefs.php (working copy)
@@ -66,4 +66,8 @@
   array('name' => 'idx_meta_cm_del', 'type' => 'index', 'fields' => array('custom_module', 'deleted')),
  ),
 );
+// HACK : -- Add localization to vardef field_help 
+// Added for adding new field help label in module builder. 
+$dictionary['FieldsMetaData']['fields']['help_label']=array('name' =>'help_label' ,'type' =>'varchar','vname'=>'COLUMN_TITLE_LABEL', 'len'=>'255');
+// HACK : -- Add localization to vardef field_help 
 ?>
Index: modules/ModuleBuilder/controller.php
===================================================================
--- modules/ModuleBuilder/controller.php (revision 1)
+++ modules/ModuleBuilder/controller.php (working copy)
@@ -269,6 +269,14 @@
         if (! empty ( $_REQUEST [ 'view_module' ] ) && !empty($_REQUEST [ 'labelValue' ]))
         {
             $_REQUEST [ "label_" . $_REQUEST [ 'label' ] ] = $_REQUEST [ 'labelValue' ] ;
+   // HACK : -- Add localization to vardef field_help
+             if (isset($_REQUEST['help_label']) && isset ($_REQUEST['help' ])){
+    if(empty($_REQUEST['help_label'])){
+              $_REQUEST['help_label']="LBL_".$_REQUEST['name']."_HELP";
+                }
+    $_REQUEST [ "label_" . $_REQUEST['help_label'] ] = $_REQUEST [ 'help' ] ;
+             } 
+            // HACK :
             require_once 'modules/ModuleBuilder/parsers/parser.label.php' ;
             $parser = new ParserLabel ( $_REQUEST['view_module'] , isset ( $_REQUEST [ 'view_package' ] ) ? $_REQUEST [ 'view_package' ] : null ) ;
             $parser->handleSave ( $_REQUEST, $GLOBALS [ 'current_language' ] ) ;
@@ -332,11 +340,22 @@
             $mb = new ModuleBuilder ( ) ;
             $module = & $mb->getPackageModule ( $_REQUEST [ 'view_package' ], $_REQUEST [ 'view_module' ] ) ;
             $field->save ( $module ) ;
+            //HACK : -- Add localization to vardef field_help
+            if(isset($_REQUEST['help_label']) && empty($_REQUEST['help_label']))
+              $_REQUEST['help_label']="LBL_".mb_strtoupper($_REQUEST['name'])."_HELP";         
+           
+   $module->mbvardefs->vardef['fields'][$_REQUEST['name']]['help']=$_REQUEST['help_label'];
+            //HACK :
             $module->mbvardefs->save () ;
             // get the module again to refresh the labels we might have saved with the $field->save (e.g., for address fields)
             $module = & $mb->getPackageModule ( $_REQUEST [ 'view_package' ], $_REQUEST [ 'view_module' ] ) ;
             if (isset ( $_REQUEST [ 'label' ] ) && isset ( $_REQUEST [ 'labelValue' ] ))
                 $module->setLabel ( $GLOBALS [ 'current_language' ], $_REQUEST [ 'label' ], $_REQUEST [ 'labelValue' ] ) ;
+            //HACK : -- Add localization to vardef field_help
+            //setting the help text and label for current language.            
+            if (isset($_REQUEST['help_label']) && isset ($_REQUEST['help']))
+              $module->setLabel($GLOBALS['current_language'],$_REQUEST['help_label'],$_REQUEST['help']);         
+            //HACK :
             $module->save();
         }
         $this->view = 'modulefields' ;
Index: modules/ModuleBuilder/views/view.modulefield.php
===================================================================
--- modules/ModuleBuilder/views/view.modulefield.php (revision 1)
+++ modules/ModuleBuilder/views/view.modulefield.php (working copy)
@@ -145,6 +145,31 @@
             if(!empty($vardef['vname'])){
                 $fv->ss->assign('lbl_value', htmlentities(translate($vardef['vname'], $moduleName), ENT_QUOTES, 'UTF-8'));
             }
+            //HACK : -- Add localization to vardef field_help
+            $temp_var_help_translation='';
+            if(isset($vardef['help'])){
+             if(!empty($vardef['help'])){
+              $temp_var_help_translation=translate(mb_strtoupper($vardef['help']),$moduleName);
+              if($temp_var_help_translation != mb_strtoupper($vardef['help'])){
+      $vardef['help_label']=$vardef['help'];
+               $vardef['help']=$temp_var_help_translation;
+     }else{
+      $vardef['help_label']=$vardef['help'];
+               $vardef['help']='';      
+     }
+             }
+         }elseif(!empty($vardef['name'])){ 
+    $tempkey="LBL_".mb_strtoupper($vardef['name'])."_HELP";
+    $temp_var_help_translation = translate(mb_strtoupper($tempkey),$moduleName);
+             if($temp_var_help_translation != mb_strtoupper($tempkey)){
+     $vardef['help_label']=$tempkey;
+              $vardef['help']=$temp_var_help_translation;
+    }else{
+     $vardef['help_label']=$tempkey;
+              $vardef['help']='';          
+    }
+         }
+            //HACK :
             //$package = new stdClass;
             //$package->name = 'studio';
             //$fv->ss->assign('package', $package);
@@ -186,6 +211,35 @@
             $fv->ss->assign('module', $module);
             $fv->ss->assign('package', $package);
             $fv->ss->assign('MB','1');
+   //HACK : -- Add localization to vardef field_help
+            $temp_var_help_translation='';
+            if(isset($vardef['help'])){ 
+             if(!empty($vardef['help'])){
+     //HACK:Upgrade sugarbase to SugarCRM 5.5
+              $temp_var_help_translation=$module->getLabel($GLOBALS['current_language'],$vardef['help']);
+     //HACK:Upgrade sugarbase to SugarCRM 5.5
+              if(!empty($temp_var_help_translation)){
+      $vardef['help_label']=$vardef['help'];
+               $vardef['help']=$temp_var_help_translation;
+     }else{
+      $vardef['help_label']=$vardef['help'];
+               $vardef['help']='';      
+     }
+             }
+         }elseif(!empty($vardef['name'])){ 
+    $tempkey="LBL_".mb_strtoupper($vardef['name'])."_HELP";
+    //HACK:Upgrade sugarbase to SugarCRM 5.5
+    $temp_var_help_translation=$module->getLabel($GLOBALS['current_language'],$tempkey);
+    //HACK:Upgrade sugarbase to SugarCRM 5.5
+             if(!empty($temp_var_help_translation)){
+     $vardef['help_label']=$tempkey;
+              $vardef['help']=$temp_var_help_translation;
+    }else{
+     $vardef['help_label']=$tempkey;
+              $vardef['help']='';          
+    }
+         }
+            //HACK :
 
             if(isset($vardef['vname']))
                 $fv->ss->assign('lbl_value', htmlentities($module->getLabel('en_us',$vardef['vname']), ENT_QUOTES, 'UTF-8'));