Table of Contents
Why Use .NET with Sage CRM?
Use .NET with Sage CRM
Master .NET with Sage CRM
Why Use .NET for Sage CRM Custom Development?
.NET Web Controls will allow you to take your Sage CRM custom development to new heights. If you’re like us at Azamba, you find yourself building out a lot of custom HTML (i.e. cool stuff) that gets injected into Sage CRM. The content we cover here will help you do that efficiently, effectively and within the confines of modern software practices.
Here’s the deal:
If the bulk of your code hinges on HTML tapestry woven of text strings, you’re flirting with poor productivity at best and disaster at worst. Let’s face it: strings are evil. When you make the decision to add code like.
string myHtml = ""; myHtml += ""; myHtml += "";
… into your .NET project, you may as well wear mittens while coding because you’re severely handicapping yourself.
The Problem with Good Ol’ Text
I won’t string this out. The reasons are straightforward and quite problematic.
- Text Lacks Modularity. Modularity implies objects and functionality have been encapsulated, making code easier to develop, read, debug, maintain, and reuse.
- There’s No Compile-Time Checking. If you make a typo (and you will) in a big block of HTML text, the compiler won’t catch it. However, when using higher-level abstractions (i.e. OOP), the compiler will enforce type checking and be your savior.
- Debugging Can Be Difficult. Catching problems in a big block of string can be quite miserable. I have time to write this article because I use strings sparingly. Enough said.
- You Don’t Get IntelliSense. IntelliSense saves lots of keystrokes for increased productivity. When you choose to represent HTML as a string instead of higher-level objects (i.e. classes), you lose out on this nifty time saver.
Object Oriented Programming (OOP) To The Rescue
Everything in the .NET Base Class Library (BCL) is heavily object-oriented and, the BCL comes with tons of goodies to make life easier. That’s great because that means developers can focus more time on solving business problems and less time on low-level plumbing and debugging those big, garbled blocks of HTML text.
Throughout this guide, we will be providing examples for some of the more common controls (tables/grids, divs/spans, textboxes, images, etc.), demonstrating how to integrate them into your Sage CRM solution.
To give you a taste of what is to come, what follows is a simple example to illustrate the power of this approach.
.NET Web Controls in Action – Consoling Isn’t It?
To demonstrate there’s no fancy setup needed to take advantage of these gems, this example uses a plain console application.
Setting up the Project
Create a .NET console application with a target framework of v3.5. We’re using .NET v3.5 because that’s the latest .NET version that works with the CRM .NET SDK.
Add references to the following .NET assemblies.
- System.Web
- System.Drawing
Add Using Statements
Now, add your using statements.
using System; using System.Drawing; using System.Web.UI.WebControls;
Add Generic Method to Get HTML
Add a helper class and static method to render (i.e. retrieve HTML text) from any object inherited from System.Web.UI.Control. Note: controls can be nested many layers deep, so always pass in the parent object.
/// Returns HTML from the parent control, including all nested/child controls. /// /// The parent control object. /// public static string GetServerControlHtml(System.Web.UI.Control ctrl) { var writer = new System.IO.StringWriter(); var htmlWriter = new System.Web.UI.HtmlTextWriter(writer); ctrl.RenderControl(htmlWriter); return writer.ToString(); } }
Making Use of the Web Controls
Lastly, add the following code to your Main method.
static void Main(string[] args) { // Panel renders as a div var div = new Panel() { BackImageUrl = "http://ignite.azamba.com/wp-content/uploads/2012/02/ignite1-png-300x85.png", Height = new Unit(85), Width = new Unit(300), BackColor = Color.LightBlue, // for illustration only BorderStyle = BorderStyle.Dotted, BorderColor = ColorTranslator.FromHtml("#0000A0"), BorderWidth = 5, HorizontalAlign = HorizontalAlign.Right }; div.Style["padding"] = "5px"; // Hyperlink renders as an anchor var link = new HyperLink() { ID = "ExampleAnchorID", CssClass = "SomeCssClassName", NavigateUrl = "http://www.sagecrm.com", Text = "Sage CRM" ToolTip = "The greatest CRM product ever!", Visible = true, // unnecessary - true is default Enabled = true // unnecessary - true is default< }; link.Attributes["name"] = "ExampleAnchorName"; // All controls inherit from System.Web.UI.Control and can be nested // as many layers as needed using ControlCollection (i.e. Control.Controls) div.Controls.Add(link); // controls inherited from System.Web.UI.Control have a RenderControl method var renderedHtml = MyHtmlBuilder.GetServerControlHtml(div); Console.WriteLine(renderedHtml); Console.ReadLine(); } }
Seeing What We Got
Okay, let’s run it! You should see the below HTML outputted to the console window.
Sage CRM
If you were to add the rendered HTML into the body of a web page, using a nifty tool like JS Bin, you should see a div and functioning hyperlink. Ta-da!
Hopefully this information has made it apparent how much better and easier your CRM development can be when you leverage web controls. .NET has everything you need out-of-the-box to start using time-tested OOP techniques for your HTML rendering needs.
In the coming sections, we will continue to explore how to make the best of these useful tools.
We wish you happy CRM coding. And please remember: no good can come from an unhealthy reliance on text strings.
Return to Top
How to Use .NET Web Controls For Sage CRM
This section continues our exploration on how to leverage out-of-the-box .NET web controls for Sage CRM development.
In the previous section, we considered the consequences of relying on text strings to represent our HTML and how we might alleviate those pains with our good friend OOP. Then, we took a brief glimpse at using the web controls provided in the .NET base class library.
Now we’re going a step further to explore a few of the basic web controls and investigate useful properties and methods, many of which are shared among the other web controls.
When to Choose This Stuff over Sage’s Stuff
We’re not going to play favorites. We believe in using the most effective tool to meet the business needs.
Sometimes this means using Sage’s API fully, sometimes very little. Sage CRM exposes a very effective API for building basic screens, lists, etc. Hence, when you can use the Sage-provided types to meet your development needs, that’s the best course of action.
The reality, however, is that more sophisticated customizations often require some level of generic web development. And, if you’re going to do that, it’s in your best interest to leverage OOP and existing code libraries as we so enthusiastically pointed out in the previous section.
.NET Web Controls in Action – Take 2
Setting up the Project
In the first section, we used a console application to keep the project simple. This time, we’ll be using CRM’s .NET API.
As such, I’ve created a dummy tab at the Company level that points to my .NET DLL (DotNetWebControls.dll) and method (RenderSamplePage). If you have no earthly idea as what we’re talking about, we suggest you check out Sage’s many resources on using the .NET API – all the cool kids are doing it, after all.
As before, I’m using a target .NET framework of v3.5.
You need to add references to the following .NET assemblies.
- System.Web
- System.Drawing
Add Using Statements
Yep, you know the drill…
using System; using System.Collections.Generic; using System.Web.UI.WebControls; using sd = System.Drawing; using Sage.CRM.WebObject; using sui = Sage.CRM.UI;
If “using sui = Sage.CRM.UI” is the first you’ve ever seen of its kind, it’s simply an alias for the namespace. There are a certain class name conflicts between the .NET and Sage namespaces we’re using (Panel, for instance), so this just prevents you from having to type in all that mess. If one of your beloved hobbies is typing, you may consider leaving out the alias.
The Code
Drum roll (or awesome guitar solo)… here’s the code to add.
namespace DotNetWebControls { public static class AppFactory { // "RenderSamplePage" is the method name Sage needs for the custom tab public static void RenderSamplePage(ref Web AretVal) { AretVal = new SamplePage(); } } public class SamplePage : Web { public override void BuildContents() { GetTabs("Company"); AddContent(HTML.Form()); // Panels render as div tags Panel div = new Panel(); div.Attributes["style"] = "margin-left:10px"; // Labels render as span tags Label span = new Label(); span.Text = "I'm a span"; // I render as a hyperlink (just kidding... I'm a textbox) TextBox txtBox = new TextBox(); // Identifiers txtBox.ID = "TextBox1"; txtBox.Attributes.Add("Name", "TextBox1"); // I see you! txtBox.Visible = true; // default value // I can use you! txtBox.Enabled = true; // default value // Specifying the text txtBox.Text = "Default text..."; // Inline styling txtBox.BorderColor = sd.Color.Blue; txtBox.BorderStyle = BorderStyle.Dotted; txtBox.BorderWidth = 3; txtBox.BackColor = sd.ColorTranslator.FromHtml("#ABCDEF"); txtBox.ForeColor = sd.Color.Red; // Sizing up the control txtBox.Height = new Unit(25, UnitType.Pixel); txtBox.Width = 200; // defaults to pixel // Font properties txtBox.Font.Size = 10; txtBox.Font.Bold = true; txtBox.Font.Italic = false; txtBox.Font.Names = new string[] { "Tahoma", "Verdana" }; HyperLink link = new HyperLink(); link.Text = ""; link.NavigateUrl = "http://ignite.azamba.com/"; link.Target = "_blank"; Image img = new Image(); img.ImageUrl = "http://ignite.azamba.com/wp-content/uploads/2012/02/ignite1-png-300x85.png"; img.ToolTip = "The best Sage CRM learning resource out there!"; // renders as a standard HTML tooltip img.ImageAlign = ImageAlign.Middle; // note here that we're adding the image as a child control to the link // ... this is the same as link.Controls.Add(img); // Let's create some data so that we can "bind" it somewhere var years = new List(); for (int i = 0; i <= 10; i++) years.Add(DateTime.Now.AddYears(i).Year.ToString()); DropDownList ddl = new DropDownList(); ddl.CssClass = "EDIT"; // here's an example of styling controls to match CRM ddl.SelectedIndex = 3; // data binding - the control will be filled with values from the list of years ddl.DataSource = years; ddl.DataBind(); // adding the controls to papa div div.Controls.Add(span); div.Controls.Add(txtBox); div.Controls.Add(link); div.Controls.Add(ddl); // applying properties globally foreach (System.Web.UI.Control ctrl in div.Controls) { // Attributes.Add can be used to add any attribute you want ((WebControl)ctrl).Attributes.Add("style", "display:block; margin-bottom:10px;"); } // Literals are literally just HTML Literal lit = new Literal(); lit.Text = "I'm literal HTML"; div.Controls.Add(lit); // add everything into a ContentBox for giggles var contentBox = new Sage.CRM.UI.ContentBox(); contentBox.Title = "Using .NET Web Controls!"; contentBox.StyleName = "VIEWBOX"; contentBox.Inner = new sui.HTMLString(MyHtmlBuilder.GetServerControlHtml(div)); AddContent(contentBox); } } public class MyHtmlBuilder { // Returns HTML from the parent control, including all nested/child controls. //The parent control object. public static string GetServerControlHtml(System.Web.UI.Control ctrl) { var writer = new System.IO.StringWriter(); var htmlWriter = new System.Web.UI.HtmlTextWriter(writer); ctrl.RenderControl(htmlWriter); return writer.ToString(); } } }
Seeing What We Get
We build the DLL and refresh the page… Boom! Now you do it.
The Code Explained
Within the code, a deliberate effort was made to use the properties, methods and techniques you need to get started using .NET web controls. In reviewing the code comments, the usage of each should be (relatively) obvious.
If not, let us know – it might make for a good future article. Please note that in demonstrating some of the styling properties, our output isn’t looking wholly Sage-like. This is the case so that the styling effects are visually apparent.
In practice, I assume you will use the CSS classes provided within Sage CRM as much as possible.
Are you now in a state of blissful euphoria on account of .NET web control awesomeness? Are you shouting from the rooftops? I thought so! OOP, OOP!
Now that we’ve taken a closer look at this topic of interest, we hope you’ve discovered some new approaches to fulfilling your more advanced Sage CRM development needs.
In the next section, we will continue our investigation of the .NET web controls but with more emphasis on higher order form controls.
Return to Top
How to Master .NET Web Controls For Sage CRM
This section wraps up our in-depth look into how to leverage .NET’s out-of-the-box web controls for Sage CRM development.
In the previous section, we explored a few basic .NET web controls (textboxes, images, hyperlinks, etc.). This time, our aim is to illustrate how you can go about building a more complex UI component from these basic controls.
This way, you can stay true to the righteous path of OOP orientation in your CRM development. Specifically, we’re going to lay the foundation for a reusable custom CRM list control.
When Using a Sage List is the Wise Thing to Do
OOTB lists are quite easy to use and I find that makes Sage CRM pretty darn powerful. Lists are one of the most complex UI components in any software solution but CRM allows you to configure a basic list in a matter of minutes. That’s pretty nifty!
Working with Sage CRM lists isn’t all bliss, however. For instance, having multiple lists on the same page breaks sorting and paging functionality. There are also several other restrictions as well that I won’t go into here.
In more advanced cases, you must either write a lot of JavaScript to manipulate the rendered list client-side or write custom server-side code. Let’s take the latter approach.
Creating Your Own List Control
For the sake of time and space, we’re going to keep this as simple as possible. But, in doing so, we want to provide solutions for a few common issues to get the ball rolling. Here are a few restrictions you’ll tackle in this basic list control.
- Allowing multiple lists to co-exist on the same page – only sorting is included in this demo
- Fixed or variable width columns
- Full control over display names
.NET Web Controls in Action – Take 3
Setting up the Project
Just like in part 2, we’ve created a dummy tab at the Company level that points to our .NET DLL (DotNetWebControls.dll) and method (RenderSamplePage). We’re also using a target .NET framework of v3.5 and the CRM .NET API as before. Lastly, we’ve added a reference to the System.Web assembly.
The Code
Alright, go ahead and add the (much lengthier) code.
using System; using System.Collections.Generic; using System.Web.UI.WebControls; using Sage.CRM.WebObject; using Sage.CRM.Utils; using sui = Sage.CRM.UI; using Sage.CRM.Data; namespace DotNetWebControls { public static class AppFactory { // "RenderSamplePage" is the method name Sage needs for the custom tab public static void RenderSamplePage(ref Web AretVal) { AretVal = new SamplePage(); } } public class SamplePage : MyCrmPage { public override void BuildContents() { GetTabs("Company"); AddContent(HTML.Form()); this.ListManager.RegisterList(GetMyPeepsList("MyPeepsList1")); this.ListManager.RegisterList(GetMyPeepsList("MyPeepsList2")); Sage.CRM.UI.ContentBox box; box = new sui.ContentBox(); box.Title = "My Peeps #1"; box.Inner = new sui.HTMLString(this.ListManager.GetListHtml("MyPeepsList1")); AddContent(box); AddContent(""); box = new sui.ContentBox(); box.Title = "My Peeps #2"; box.Inner = new sui.HTMLString(this.ListManager.GetListHtml("MyPeepsList2")); AddContent(box); base.BuildContents(); } private static MyList GetMyPeepsList(string htmlId) { MyList list = new MyList(htmlId); list.DefaultSortField = "Pers_LastName"; list.DefaultSortDirection = SortDirection.Descending; list.Attributes.Add("width", "100%"); list.SQL = @"SELECT Pers_FirstName, Pers_LastName, Pers_Title FROM Person"; MyListColumn column; column = new MyListColumn(); column.FieldName = "Pers_FirstName"; column.DisplayName = "First Name"; column.Width = new Unit(250); list.Columns.Add(column); column = new MyListColumn(); column.FieldName = "Pers_LastName"; column.DisplayName = "Last Name"; column.Width = new Unit(250); list.Columns.Add(column); column = new MyListColumn(); column.FieldName = "Pers_Title"; column.DisplayName = "Title"; column.IsSortable = false; column.HorizontalAlign = HorizontalAlign.Center; list.Columns.Add(column); return list; } } public abstract class MyCrmPage : Web { public readonly bool IsPostback = false; public readonly MyListManager ListManager = new MyListManager(); public Dispatch CrmDispatch; public MyCrmPage() { ListManager.Page = this; CrmDispatch = this.Dispatch; IsPostback = Dispatch.ContentField("isPostBack") == "true"; } public override void BuildContents() { AddContent(""); } } public class MyListManager { private Dictionary<string, MyList> _lists = new Dictionary<string, MyList>(); public MyCrmPage Page; public IEnumerable Lists { get { return _lists.Values; } } public void RegisterList(MyList list) { if(ListIdExists(list)) throw new Exception("HtmlId must be unique."); _lists.Add(list.HtmlId, list); } private bool ListIdExists(MyList list) { foreach (MyList registeredList in _lists.Values) { if (list.HtmlId.Equals(registeredList.HtmlId, StringComparison.CurrentCultureIgnoreCase)) return true; } return false; } public string GetListHtml(string htmlId) { if (!_lists.ContainsKey(htmlId)) throw new Exception("List must be registered in ListManager"); var list = _lists[htmlId]; if (Page.IsPostback) { var content = Page.CrmDispatch.ContentFields(); list.DefaultSortField = Page.CrmDispatch.ContentField(list.HtmlId + "_HIDDENORDERBY"); var orderByDesc = Page.CrmDispatch.ContentField(list.HtmlId + "_HIDDENORDERBYDESC"); if (orderByDesc == (true).ToString()) list.DefaultSortDirection = SortDirection.Descending; else list.DefaultSortDirection = SortDirection.Ascending; } return _lists[htmlId].GetHtml(); } } public class MyList { public readonly string HtmlId; public Dictionary<string, string> Attributes = new Dictionary<string, string>(); public string SQL; public List Columns = new List(); public string DefaultSortField; public SortDirection DefaultSortDirection = SortDirection.Ascending; public MyList(string htmlId) { if (string.IsNullOrEmpty(htmlId.Trim())) throw new Exception("HtmlId is empty or missing."); else HtmlId = htmlId.Trim(); } public string GetHtml() { Panel pnl = new Panel(); HiddenField orderByField = new HiddenField(); orderByField.ID = HtmlId + "_HIDDENORDERBY"; orderByField.Value = DefaultSortField; pnl.Controls.Add(orderByField); HiddenField orderByDirection = new HiddenField(); orderByDirection.ID = HtmlId + "_HIDDENORDERBYDESC"; orderByDirection.Value = (DefaultSortDirection == SortDirection.Descending).ToString(); pnl.Controls.Add(orderByDirection); Table grid = new Table(); grid.Attributes["style"] = "padding:5px;"; foreach (KeyValuePair<string, string> kvp in Attributes) { grid.Attributes.Add(kvp.Key, kvp.Value); } // add column header row grid.Rows.Add(GetHeaderRow()); // add data rows grid.Rows.AddRange(GetDataRows()); pnl.Controls.Add(grid); return MyHtmlBuilder.GetServerControlHtml(pnl); } private TableRow GetHeaderRow() { TableRow gridRow = new TableRow(); string sortJavascript = @"var originalSortField = $('#{0}_HIDDENORDERBY').val(); var newSortField = '{1}'; if (originalSortField == newSortField) {{ if ($('#{0}_HIDDENORDERBYDESC').val() == 'True') $('#{0}_HIDDENORDERBYDESC').val('False'); else $('#{0}_HIDDENORDERBYDESC').val('True'); }} else {{ $('#{0}_HIDDENORDERBYDESC').val('False'); }} $('#{0}_HIDDENORDERBY').val('{1}'); document.EntryForm.submit();"; foreach (MyListColumn column in Columns) { TableCell gridCell = new TableCell(); gridCell.CssClass = "GRIDHEAD"; gridCell.Attributes["style"] = "border:0px;padding-bottom:1px;margin:0px;"; gridCell.HorizontalAlign = column.HorizontalAlign; gridCell.Width = column.Width; if (column.IsSortable) { HyperLink anchor = new HyperLink(); anchor.CssClass = "GRIDHEADLINK"; anchor.Text = column.DisplayName; anchor.Attributes["href"] = string.Format("Javascript:" + sortJavascript, HtmlId, column.FieldName); gridCell.Controls.Add(anchor); Literal blankSpace = new Literal(); blankSpace.Text = "&nbsp;"; gridCell.Controls.Add(blankSpace); if (column.FieldName == DefaultSortField) { Image arrow = new Image(); if (DefaultSortDirection == SortDirection.Ascending) arrow.ImageUrl = "/crm_azamba/Themes/img/color/Buttons/up.gif"; else arrow.ImageUrl = "/crm_azamba/Themes/img/color/Buttons/down.gif"; arrow.ImageAlign = ImageAlign.Top; gridCell.Controls.Add(arrow); } } else { gridCell.CssClass = "GRIDHEAD"; gridCell.Text = column.DisplayName; } gridRow.Cells.Add(gridCell); } return gridRow; } private TableRow[] GetDataRows() { List dataRows = new List(); TableRow gridRow; QuerySelect qry = new QuerySelect(); qry.SQLCommand = SQL; // default the first column if (string.IsNullOrEmpty(DefaultSortField.Trim())) qry.SQLCommand += " ORDER BY " + Columns[0].FieldName; else qry.SQLCommand += " ORDER BY " + DefaultSortField; if (DefaultSortDirection == SortDirection.Descending) qry.SQLCommand += " DESC"; qry.ExecuteReader(); int rowCount = 1; while (!qry.Eof()) { gridRow = new TableRow(); if (rowCount % 2 == 1) gridRow.CssClass = "ROW2"; else gridRow.CssClass = "ROW1"; foreach (MyListColumn column in Columns) { TableCell gridCell = new TableCell(); gridCell.HorizontalAlign = column.HorizontalAlign; var value = qry.FieldValue(column.FieldName).ToString(); if (string.IsNullOrEmpty(value)) gridCell.Text = "&nbsp;"; else gridCell.Text = value; gridRow.Cells.Add(gridCell); } dataRows.Add(gridRow); rowCount++; qry.Next(); } return dataRows.ToArray(); } } public class MyListColumn { public string FieldName; public string DisplayName; public bool IsSortable = true; public HorizontalAlign HorizontalAlign = HorizontalAlign.Left; public Unit Width; } public class MyHtmlBuilder { public static string GetServerControlHtml(System.Web.UI.Control ctrl) { var writer = new System.IO.StringWriter(); var htmlWriter = new System.Web.UI.HtmlTextWriter(writer); ctrl.RenderControl(htmlWriter); return writer.ToString(); } } }
Seeing What We Get
Let’s get the results out of the way before I move on to an explanation. Go ahead and build your project, then refresh the Web Controls tab in CRM. You should get two lists that display the same content. In the screenshot provided below, I’ve sorted the lists differently to show that sorting works correctly for both lists.
The Code Explained – from 10,000 feet
There’s a lot of code in this example, so I’ll just touch on the highlights. If you have specific questions, just let me know in the comments section at the bottom of the page.
- MyCrmPage. To limit code duplication, I’ve created a page-level class. In this example, it’s being used to handle list management. This class inherits from Sage’s Web class then my custom page inherits from MyCrmPage.
- MyListManager. This is the class that allows the base class to be able to apply sorting to each list. Basically we “register” the list with the page and then the page knows how to apply sorting to each list via the ListManager instance.
- MyList. This class handles the HTML generation for a given list. It holds list-level properties, which includes a collection of list columns. Table, TableRow, TableCell, and HiddenField are all new .NET web controls to this series and are used to build out the HTML table and maintain state between postbacks.
- MyListColumn. Lastly, this class maintains column-specific properties.
Next Steps
Whew. That’s some good stuff!
Over the course of this page, we’ve had a fantastic time sharing the many positive benefits of using .NET web controls with you. We sincerely hope you’ve been inspired to leverage these tools to bring even more value to Sage CRM.
Ultimately, it’s up to us to solve business problems and ensure the user experience is pleasurable and easy to use. The foundation for a custom list control has been paved.
Now it’s up to you to go fire up that copy of Visual Studio and … build away fellow CRM coder!
Until next time, here’s to wishing you happy CRM coding.
Disclaimer: All code is provided as is and is meant for instructional purposes only. This code has not been tested for production use nor does it incorporate comprehensive error handling.
ALWAYS BACKUP YOUR SYSTEM AND USE A TEST ENVIRONMENT WHEN MAKING CHANGES.