Friday, August 4, 2017

Update content type workflow association approver through PowerShell


I had a requirement to update an approver on a SharePoint workflow which was setup on Document Content Type at the library level. The workflow has been there since SharePoint 2010 and we upgraded to 2013 around 3 years ago. This was an OOB workflow created in UI  (not through SPD) with single task approver with CC user added (see the XML snippet below)

When I opened up workflow settings and in the "Change Workflow" page, I am not seeing plain "Approval" workflow template which is the actual workflow template used but not "Approval - SharePoint 2010". Also the text box is disabled/greyed out and the system isn't letting me change the template throwing the error prompt. Not sure if this is by design or a bug in SharePoint.  


Change workflow page -  Selection is disabled
System don't let you change the template

Anyways, I needed to update the workflow approver with a new employee because the existing one was leaving. When I figured I can't update through UI then I tried through SharePoint Designer which wasn't letting me do it either. So I have chosen PowerShell route to tweak the workflow association properties. 

Association Data in XML


PowerShell snippet.

Just retrieve the list content type's workflow association data which is an xml string and update the approver values.

             $web = Get-SPWeb "http://<SharePoint web URL>"                    
        $list = $web.Lists | Where-Object { $_.Title -eq 'List Title' }                       
        $ListWorkFlowAssns =  $list.WorkflowAssociations        
        foreach($ctype in $list.ContentTypes)
        {
            if ($ctype.Name -eq "Document")
            {                
                $wa = $ctype.WorkflowAssociations.GetAssociationByName("My Approval", $web.UICulture)
                if ($wa -ne $null)
                {                                                                                  
                    $parsedXml = [System.Xml.Linq.XElement]::Parse($wa.AssociationData)                
                    $nodes = $parsedXml.Descendants() | Where-Object {($_.Name.LocalName -eq "Reviewers")}                                    
                    $accountId = $nodes[0].Descendants() | Where-Object {($_.Name.LocalName -eq "AccountId")}
                    $displayName = $nodes[0].Descendants() | Where-Object {($_.Name.LocalName -eq "DisplayName")}
                    $accountId.SetValue("Domain\UserName") # replace value with the new approver AD username
                    $displayName.SetValue("User Full Name") # replace value with the new approver AD Full Name                 
                    $wa.AssociationData = $parsedXml.ToString()
                    $ctype.WorkflowAssociations.Update($wa)               
                    
                }               
            }            
        }

        $list.Update()




I made sure no current instances are running and no new instances are created while I am making these changes. 






Note: If you are following the same approach, please put additional checks and filters while updating. You don't want to update other content types mistakenly or workflow association properties that you are not intended. 

Monday, April 24, 2017

Attended SharePoint Fest DC 2017


Last week I attended the SharePoint conference in Washington DC- SharePoint Fest DC 2017. It has various tracks such ECM. Search, BI, Office 365, workflow, and development. I have chosen all the developer and implementation topics with the focus on Azure, Office 365 related to SharePoint. 




The development topics that I attended were touched on React, TypeScript, SPFx, SharPoint PnP. I had chance attend sessions from SharePoint's well known MVPs such as Joel Oleson, Asif Rehmani (remember SharePoint workflow vidoe's); few presenters were very young who seemed hard core javascripters. 

Here are the topics that I attended:
  • How to make use of All that’s included in Office 365 – By Asif Rehmani
  • SharePoint Frame work, Angular & Azure Functions: The modern SharePoint developer tool belt – By Sebastien Levert
  • Using Document Sets to implement Business Processes – By Tom Robbins
  • Not sure how you should REACT – By Ryan Schouten
  • Teams, Groups, SharePoint and Yammer….Oh My! – By Joel Oleson
  • Upgrading Legacy SharePoint Customizations to the Add-in Model – By Danny Jessee
  • SharePoint 2016 : What’s New and Why should I upgrade? – By Paul Stark
  • Supercharge your SharePoint Framework Webpart with React – By Eric Overfield
  • Extranets in SharePoint On Premises and Office 365 – By Peter Carson
  • Azure Active Directory (Azure AD) for Office 365 Developers – By Prashanth G Bhoyar

Overall, I liked the conference; came out with one aspiration: do more JavaScript, get on the cloud....

Thursday, January 26, 2017

SharePoint Custom WCF Service with Automatic Formatting



Recently I was writing a custom SharePoint WCF service for using in client side JavaScript libraries (jquery, angular) and C# server side call HttpWebRequest without worrying adding references and proxy classes. Majority of the articles I referred to were only showing JSON output; I wanted this service to provide both JSON and Xml data formats. If the requirement is to output both JSON and Xml formats, I found people writing two method calls:one for each JSON  and Xml. Also, I have seen articles where response was driven through method or query string parameter to get requirement format (WebMessageFormat.Xml, WebMessageFormat.Json) If I follow the former approach as shown below, the amount of code will grow twice the code that I actually need which is very inefficient and demand more maintenance. 

[ServiceContract]
interface IPersonService

    {
         [WebGet(UriTemplate = "person/{id}",  ResponseFormat = WebMessageFormat.Json)]
          Person GetPersonJSON(int id); //get JSON output

         [WebGet(UriTemplate = "person/{id}",  ResponseFormat = WebMessageFormat.Xml)]
          Person GetPersonXml(int id);  //get Xml output
    }

All I wanted was automatic formating based on some configuration or flag set through code. While searching for options. I found this technet article. helpful.
WCF Web HTTP Automatic and Explicit  Formatting options
https://msdn.microsoft.com/en-us/library/ee476510(v=vs.110).aspx

(snippet from technet)

Automatic Formatting

The WCF Web HTTP programming model allows you to dynamically determine the best format for a service operation to return its response in. Two methods for determining an appropriate format are supported: automatic and explicit.
When enabled, automatic formatting chooses the best format in which to return the response. It determines the best format by checking the following, in order:
  1. The media types in the request message’s Accept header.
  2. The content-type of the request message.
  3. The default format setting in the operation.
  4. The default format setting in the WebHttpBehavior
(/snippet from technet)

Solution:
  1. Added web.config file to my custom wcf custom service. 
  2. C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\15\isapi\<MyCustomWCFService>
  3. Added System.ServiceModel section as show below.
         <configuration>
     <system.serviceModel>
      <behaviors>  
        <endpointBehaviors>        

          <behavior>          

            <webHttp automaticFormatSelectionEnabled="true" />
          </behavior>
        </endpointBehaviors>
      </behaviors>
      </system.serviceModel>
    </configuration>

     
Have only service contract method.


[ServiceContract]
interface IPersonService
    {
         [WebGet(UriTemplate = "person/{id}")]
          Person GetPerson(int id);
   }

Now I can the call same method through jquery/ajax by specifying the ContentType or Accept Header and get the output format that I need. 

JavaScript - JSON Output 


function getPerson() {
            var serviceUri = _spPageContextInfo.webAbsoluteUrl" + "/_vti_bin/<MyCustomService>/PersonService.svc/person/" + id;
            $.ajax({
                type: "GET",
                contentType: "application/json",
                url: serviceUri,
                dataType: "json"
            }).done(function (data) {
                //do something here
            }).fail(function (error) {
            });

        }

JavaScript - Xml Output

function getPerson() {
            var serviceUri = _spPageContextInfo.webAbsoluteUrl" + "/_vti_bin/<MyCustomService>/PersonService.svc/person/" + id;
            $.ajax({
                type: "GET",
                contentType: "application/xml",
                url: serviceUri,
                dataType: "xml"
            }).done(function (data) {
                //do something here
            }).fail(function (error) {
            });

        }

Server Side C# - HttpWebRequest - JSON Output


HttpWebRequest endpointRequest = (HttpWebRequest)HttpWebRequest.Create(serviceUri);
endpointRequest.Method = "GET";
endpointRequest.Accept = "application/json;odata=verbose";



Server Side C# - HttpWebRequest - Xml Output


HttpWebRequest endpointRequest = (HttpWebRequest)HttpWebRequest.Create(serviceUri);
endpointRequest.Method = "GET";
endpointRequest.Accept = "application/xml;odata=verbose";

Friday, October 28, 2016

SharePoint 2013 Search Crawl


SharePoint 2013 Search crawls stuck

The search crawls are getting hung for some reason. It all started recently. The crawls were stuck for hours and days if  not stopped manually. The farm that I work with would normally finish full crawls in less than 4-5 hours with around 300K size index in the largest content source. Strangely enough, when I kick off the crawl, the crawl is running fine with index count grows, however, I see no commits at the end (crawl never completes). Below are ULS errors I extracted. 

Search service events aei87 Warning A database error occurred. Source: .Net SqlClient Data Provider Code: 220 occurred 0 time(s) Description:  Error ordinal: 1 Message: Arithmetic overflow error for data type smallint, value = 32768., Class: 16, Number: 220, State: 1    at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)     at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)     at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)     at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)     at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader ds)     at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean asyncWrite)     at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite)     at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()     at Microsoft.Office.Server.Data.SqlSession.ExecuteNonQuery(SqlCommand command)     at Microsoft.Office.Server.Search.ManagedSqlSession.ExecuteNonQuery()

mssearch.exe (0x3D7C) 0x3384 SharePoint Server Search Crawler:Gatherer Plugin ac6s7 High ManagedSqlSession caught a SqlException executing the following command: 'proc_MSS_CommitTransactions'. Connection string was: 'Data Source=XXXXX;Initial Catalog=SSA_CrawlStoreDB;Integrated Security=True;Enlist=False;Pooling=True;Min Pool Size=0;Max Pool Size=100;Connect Timeout=15;Application Name=SharePoint[OWSTIMER][1][Search_Service_Application_CrawlStoreDB_299952a3060f4f628def625b742db001]'  Original exception message: 'Arithmetic overflow error for data type smallint, value = 32768.  Duplicate key was ignored.'  Stack trace '   at System.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)     at System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)     at System.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)     at System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)     at System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async, Int32 timeout, Task& task, Boolean asyncWrite, SqlDataReader ds)     at System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, TaskCompletionSource`1 completion, Int32 timeout, Task& task, Boolean asyncWrite)     at System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(TaskCompletionSource`1 completion, String methodName, Boolean sendToPipe, Int32 timeout, Boolean asyncWrite)     at System.Data.SqlClient.SqlCommand.ExecuteNonQuery()     at Microsoft.Office.Server.Data.SqlSession.ExecuteNonQuery(SqlCommand command)     at Microsoft.Office.Server.Search.ManagedSqlSession.ExecuteNonQuery()' SQL Errors:  Error ordinal: 1 Message: Arithmetic overflow error for data type smallint, value = 32768., Class: 16, Number: 220, State: 1  Error ordinal: 2 Message: Duplicate key was ignored., Class: 0, Number: 3604, State: 0

mssearch.exe (0x3D7C) 0x3384 SharePoint Server Search Crawler:Gatherer Plugin aau6c High CManagedSqlSession::ExecuteNonQuery m_ManagedSqlSession->ExecuteNonQuery fails with Error 0x80131904 Description 'Arithmetic overflow error for data type smallint, value = 32768.  Duplicate key was ignored.' Message 'Unknown error 0x80131904'  [gathersql.cxx:2243]  search\native\gather\server\gathersql.cxx

mssearch.exe (0x3D7C) 0x3384 SharePoint Server Search Crawler:Gatherer Plugin aau2i High CGatherer::CommitTransactions pSession->ExecuteNonQuery for proc_MSS_CommitTransactions failed with Error 0x80131904  [gatherobj.cxx:8212]  search\native\gather\server\gatherobj.cxx



#1 Actions Taken:


  • Reset Search Index

Index reset resulted the above errors go away. May be this is due to the reason that our full crawls were stuck on completing forever and every time we stop and initiate the crawl, a new full crawl kicks in and get stuck again. So with all this pile up, maybe the temp table sequences in proc_MSS_CommitTransactions procedure go out of smallint limit. Not sure if this theory is right but reset index definitely cleared the above errors but the crawls were getting stuck again with a new set of errors. See below.

mssearch.exe (0x0FA8)                     0x1644      SharePoint Server Search           Crawler:Gatherer Service            dsg6  Medium        CRobotThread::Thread failed to move file for docID 50093, URL sts4://site/siteurl=site1/subsite/siteid={0a3cf259-90dc-4abb-bf2e-15bb0d7275e7}/weburl=shared/webid={c54d4bd4-f6a4-4813-9aa8-927039e53575}/listid={20e29c27-f4dc-48fb-8528-8e0bcae2d450}/folderurl=/itemid=1211, Error 0x80070005. The transaction will be retried.  [robotthrd.cxx:4810]  search\native\gather\gthrsvc\robotthrd.cxx 54ad911a-fc69-4897-920d-f557c1cf9d53


#2 Actions Taken:


  • Add Antivirus exclusions (if not already added) on all SharePoint servers.
  • Reboot the servers


Result: Crawls completed as expected. The issue is resolved. 

Saturday, January 23, 2016

JQuery and SP Modal Dialog HTML option



SharePoint SP Modal Dialog accepts either URL or html in the dialog options. The below code snippets show how to create HTML DOM elements using JQuery and pass the container HTML for rendering in the dialog. I chose this option because creating DOM elements with styles and binding them with events using JQuery is easy as compared to plain JavaScript. Using this approach we can create dynamic forms to post/get ajax requests through modal dialogs. 

<script type='text/javascript'>

//Container DIV
var container = $("<div/>").css({
        'width': '80%',
        'margin': '10px 5px 10px 20px',
        'border': '1px solid #808080',
        'padding': '10px 10px 10px 10px'
    });

//Text area for entering comments
var txtcomments = $("<textarea/>",

        { id: 'txtComments', rows: '5', cols: '50' }).css({ 'display': 'block' });


//Button added to DIV
var btn = $("<input/>", { type: 'button', value: 'Save' })
.css({
   'height': '30px',
   'width': '100px',
   'display': 'inline-block',
   'padding-right': '10px'
});

//attach button click event
btn.on("click",

        function () {
        //call ajax or WebMethod and submit data to server
        });

//Spacer DIV
var spacer = $("<div/>").css({ 'height': '30px', 'display': 'block' });

//Append all individual DOM elements to container DIV
container.append(txtcomments)
         .append(btn)
         .append(spacer);

//Dialog options
//key thing is container.get(0) which gives actual DOM element 
var dlgOptions = {
        title: "Form Dialog",
        html: container.get(0), //this is important
        width: 600,
        height: 300,
        dialogReturnValueCallback: function (result, retVal) {  //call back to refresh calling page (parent window)         
            SP.UI.ModalDialog.RefreshPage(SP.UI.DialogResult.OK);
        }
    };

//Once all values are set, on onclick event (client side) of a button call
function onClientButtonClick(){
  SP.SOD.execute('sp.ui.dialog.js', 'SP.UI.ModalDialog.showModalDialog', dlgOptions);
}
</script>

<!-- HTML Source -->
<!-- this button opens modal dialog -->
<input type='button' onclick='return onClientButtonClick();return false;' value='Open Dialog' style='width:120px' />

References
https://msdn.microsoft.com/en-us/library/office/ff410058(v=office.14).aspx
https://api.jquery.com/get/



Thursday, June 5, 2014

PSConfig stuck - SharePoint 2013 Server Service Pack 1 installation



Last week I was installing SharePoint 2013 Service Pack 1 on production environment at my workplace. The environment has 2 WFE, 2 APP, 1 DB Server.

Used the script provided by Russ Maxwell from this site below to save time on installation. Thanks Russ.
http://blogs.msdn.com/b/russmax/archive/2013/04/01/why-sharepoint-2013-cumulative-update-takes-5-hours-to-install.aspx

Installation went well and while running PSConfig Wizard on one of the app server, the process got stuck for an hour or so on Step 9 of 10. I sensed something wrong as other servers finished within less than 20 minutes. So killed the PSConfig process in Task Manager and restarted again, no luck, same hang.
Googled for some time and found this link about registry permissions
https://techsps.wordpress.com/2013/11/26/psconfig-stuck-at-10-00/

Assuming the reason for hang could be, process must be waiting on something. So checked the permissions on app and web servers. Found that, the app server (non central admin server) which is having the issue, missing permission for WSS_Restricted_WPG_V4 on the below registry:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\15.0\Secure

Gave the same permission (Full Control) as other servers and ran the PSConfig again. It finished successfully within less than 10 minutes.

Hope it helps if anyone is having the same issue.






Tuesday, October 8, 2013

Charting is easy with JqPlot, LINQ, ASP.NET, C#


This post shows a simple approach to render charts using JQPlot javascript plugin in an ASP.NET page. I have created sample data here but in actual case you could get it from database or a SharePoint list or any other data source. I am using Page.ClientScript.RegisterClientScriptBlock approach to inject the JSON object through client script block and later using JQuery document ready event to read the JSON object. The other approach could be making an Ajax call to the server side method [WebMethod] and directly fetch the data. It's up to the requirement.

Download the JQPlot here. It's free.
Assuming we already have an ASPX page in ASP.NET web application. 


Code-behind (cs)

1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.UI; 6 using System.Web.UI.WebControls; 7 using System.Web.Script; 8 using System.Web.Script.Serialization; 9 10 public partial class jqplots : System.Web.UI.Page 11 { 12 protected void Page_Load(object sender, EventArgs e) 13 { 14 if (!Page.IsPostBack) 15 { 16 //collect the data 17 List<Issue> listIssues = new List<Issue>(); 18 listIssues.Add(new Issue() { Title = "Page loads very slow", Priority = "Medium" }); 19 listIssues.Add(new Issue() { Title = "Page validation error", Priority = "High" }); 20 listIssues.Add(new Issue() { Title = "Date format showing 12:00:00 AM", Priority = "Low" }); 21 listIssues.Add(new Issue() { Title = "Table borders", Priority = "Low" }); 22 listIssues.Add(new Issue() { Title = "Some values not saving", Priority = "Low" }); 23 24 //LINQ Group by 25 var ipCounts = from i in listIssues 26 group i by i.Priority into g 27 select new { Category = g.Key, Value = g.Count() }; 28 29 //Anonymous type 30 var ip = new { issuePriority = ipCounts.ToList() }; 31 32 string issuePriorityTotals = "var issueTotals = " + new JavaScriptSerializer().Serialize(ip) + ";"; 33 34 //render javascript block to access the object from JQuery document ready function 35 Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "IP", issuePriorityTotals, true); 36 } 37 } 38 39 [Serializable] 40 public class Issue 41 { 42 public string Title { get; set; } 43 public string Priority { get; set; } 44 } 45 }

Page ViewSource (don't copy this)
If you check the browser view source after the page loads, you should see the script block like the one below. The JSON object issueTotals is the key here. You will have to read the object and construct an array and then use it JqPlot chart method. See the JavaScript code in HTML source in the next section. 

1 <script type="text/javascript">
2 //<![CDATA[
3 var issueTotals = {"issuePriority":[{"Category":"Medium","Value":1},{"Category":"High","Value":1},{"Category":"Low","Value":3}]};//]]>
4 </script>

HTML Source (aspx)


     
  • Print
  •  
  • About BlogTrog Code Window
 1 <%@ Page Language="C#" AutoEventWireup="true" CodeFile="jqplots.aspx.cs" Inherits="jqplots" %>
 2 
 3 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 4 
 5 <html xmlns="http://www.w3.org/1999/xhtml">
 6 <head runat="server">
 7     <title>JQPlot Demo</title>
 8     <script src="Scripts/common/jquery.min.js" type="text/javascript"></script>
 9     <script src="Scripts/jqplot/jquery.jqplot.min.js" type="text/javascript"></script>
10     <script src="Scripts/jqplot/jqplot.canvasTextRenderer.min.js" type="text/javascript"></script>
11     <script src="Scripts/jqplot/jqplot.canvasAxisTickRenderer.min.js" type="text/javascript"></script>
12     <script src="Scripts/jqplot/jqplot.categoryAxisRenderer.min.js" type="text/javascript"></script>
13     <script src="Scripts/jqplot/jqplot.pointLabels.min.js" type="text/javascript"></script>
14     <script src="Scripts/jqplot/jqplot.barRenderer.min.js" type="text/javascript"></script>
15     <script src="Scripts/jqplot/jqplot.pieRenderer.min.js" type="text/javascript"></script>
16     <link href="Scripts/jqplot/jquery.jqplot.min.css" rel="stylesheet" type="text/css" />
17 
18 
19     <script type="text/javascript">
20         $(document).ready(function () {
21             var data = [];            
22             if (issueTotals) {
23                 if (typeof issueTotals.issuePriority != "undefined") {
24                     var obj = issueTotals.issuePriority;
25                     for (var i = 0; i < obj.length; i++) {
26                         data.push([obj[i].Category, obj[i].Value]);
27                     }
28                 }
29             }
30 
31             $('#chart1').jqplot([data], {
32                 title: 'Issue Priority Totals - Bar chart',
33                 // Provide a custom seriesColors array to override the default colors.
34                 seriesColors: ['#85802b', '#00749F', '#73C774', '#C7754C', '#17BDB8'],
35                 seriesDefaults: {
36                     renderer: $.jqplot.BarRenderer,
37                     rendererOptions: {
38                         // Set varyBarColor to true to use the custom colors on the bars.
39                         varyBarColor: true,
40                         barWidth: 60,
41                         barMargin: 5
42                     }
43                 },
44                 axesDefaults: {
45                     tickOptions: { formatString: '%d' }
46                 },
47                 axes: {
48                     xaxis: {
49                         renderer: $.jqplot.CategoryAxisRenderer
50                     }
51                 }
52             });
53 
54             $.jqplot('chart2', [data], {
55                 grid: {
56                     drawBorder: true,
57                     drawGridlines: false,
58                     background: '#ffffff',
59                     shadow: false
60                 },
61                 title: 'Issue Priority Totals - Pie chart',
62                 axesDefaults: {
63 
64             },
65             seriesDefaults: {
66                 renderer: $.jqplot.PieRenderer,
67                 rendererOptions: {
68                     showDataLabels: true
69                 }
70             },
71             legend: {
72                 show: true,
73                 rendererOptions: {
74                     numberRows: 2
75                 },
76                 location: 's'
77             }
78         });
79 
80         });
81     </script>  
82     
83 </head>
84 <body>
85     <form id="form1" runat="server">
86     <div>
87         <div id="chart1" style="margin-top:20px; margin-left:20px; width:400px; height:400px; float:left; margin-right:10px;"></div>
88         <div id="chart2" style="margin-top:20px; margin-left:20px; width:400px; height:400px; float:left"></div>        
89     </div>   
90     </form>
91 </body>
92 </html>




Output




References