Showing posts with label c#. Show all posts
Showing posts with label c#. Show all posts

Friday, February 6, 2009

Rounding to the nearest 1000 in C#

Working on that demo app, this came up - I wanted a number rounded to the nearest 1000. This is easy enough in Excel - the ROUND function takes negative values, so that =ROUND(2009,-3) will give you 2000. The Math.Round function in System.dll doesn't offer this functionality however - you have to implement that yourself. Here's an example of one way to do this:

using System;
 
static class Program
{
    static void Main(string[] args)
    {
        double d = (double)new Random().Next();
        // round to the nearest 1000
        Console.WriteLine("{0} rounded to the nearest thousand is {1}", d, Round(d, -3));
        // round to the nearest 100 etc
        Console.WriteLine("{0} rounded to the nearest hundred is {1}", d, Round(d, -2));
    }
 
    static double Round(double value, int digits)
    {
        if ((digits < -15) || (digits > 15))
            throw new ArgumentOutOfRangeException("digits", "Rounding digits must be between -15 and 15, inclusive.");
 
        if (digits >= 0)
            return Math.Round(value, digits);
 
        double n = Math.Pow(10, -digits);
        return Math.Round(value / n, 0) * n;
    }
 
    static decimal Round(decimal d, int decimals)
    {
        if ((decimals < -28) || (decimals > 28))
            throw new ArgumentOutOfRangeException("decimals", "Rounding decimals must be between -28 and 28, inclusive.");
 
        if (decimals >= 0)
            return decimal.Round(d, decimals);
 
        decimal n = (decimal)Math.Pow(10, -decimals);
        return decimal.Round(d / n, 0) * n;
    }
}

SharePoint API: A Client-side Object Model - Demo Application

Back in November we released HubKey's Client-side API for SharePoint. This API wraps many of SharePoint Server 2007's Web Services (Lists Web Service, Webs Web Service, etc.) to provide a familiar object model that can be used on remote client machines without having to process XML requests and responses.

We've recently made a few updates and have included a demonstration application which is shown running in the screen capture below. This demo app creates a new list on a remote test site with a quick launch link, creates a "test person" list scoped content type, adds a number of randomly generated people records, and then adds a new default view with a custom query returning people below age 65. This list data is then browsable and editable by a DataGridView hosted in a Windows form. In addition, paging is demonstrated by using the SPListItemCollectionPosition object.

You can download a demo copy of the API - click here for details.

Wednesday, January 14, 2009

Copy and Paste CRM Picklist Values - Part III

Following on from Part II which went over setting up a Visual Studio project to customize the out-of-the-box edit attribute form in Microsoft Dynamics CRM 4.0, here's the last installment in this series - the code you'll need to make it work.

Open the editAll.aspx file you created in Step II and paste in the following markup:

(By the way if the carriage returns don't paste when you copy this into Visual Studio, copy and paste into and out of WordPad or MS Word first).

<%@ Assembly Name="HubKey.Crm.Web.Tools.SystemCustomization.Attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=PUBLICKEYGOESHERE" %>
<%@ Page language="c#" Inherits="HubKey.Crm.Web.Tools.SystemCustomization.Attributes.EditAllPage"    %>
<%@ Register TagPrefix="frm" Namespace="Microsoft.Crm.Application.Forms" Assembly="Microsoft.Crm.Application.Components.Application" %>
<%@ Register TagPrefix="cnt" Namespace="Microsoft.Crm.Application.Controls" Assembly="Microsoft.Crm.Application.Components.Application" %>
<%@ Register TagPrefix="ui" Namespace="Microsoft.Crm.Application.Components.UI" Assembly="Microsoft.Crm.Application.Components.UI" %>
<%@ Register TagPrefix="loc" Namespace="Microsoft.Crm.Application.Controls.Localization" Assembly="Microsoft.Crm.Application.Components.Application" %>
 
<html>
<head>
    <cnt:AppHeader runat="server" id="crmHeader"/>
</head>
<body>
<frm:DialogForm id="crmForm" runat="server">
    <table cellpadding="0" cellspacing="5" width="100%" style="table-layout: fixed;">
        <col width="100"><col>
            <tr>
                <td class="ms-crm-Field-Required"><label for="txtLabel"><%=HubKey.Crm.Web.Tools.SystemCustomization.Attributes.Globals.LOCID_EDIT_ALL_VALUE_LABEL_PAIRS%><img src="/_imgs/frm_required.gif" alt="<loc:Text Encoding='HtmlAttribute' ResourceId='Forms.Required.AltTag' runat='server'/>"/></label></td>
                <td><ui:TextArea id="txtLabel" Height="270px" runat="server"/></td>
            </tr>
    </table>
</frm:DialogForm>
</body>
</html>


You'll also need to make a change to the manageAttribute.aspx page. At the top of the page, add the following markup to register an "hk" tag prefix:

<%@ Register TagPrefix="hk" Namespace="HubKey.Crm.Web.Tools.SystemCustomization.Attributes" Assembly="HubKey.Crm.Web.Tools.SystemCustomization.Attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=PUBLICKEYGOESHERE" %>


Next, search for the following element id: "ledtPicklistValues". Insert a new table row / table data cell above the tr container for this element and paste in a new element:

<hk:AppListEditAll id="appListEditAll" runat="server"/>


The enclosing table markup should now look like the following:

<table width="100%" cellspacing="0" cellpadding="0">
 
<tr class="param">
<td><hk:AppListEditAll id="appListEditAll" runat="server"/></td>
</tr>
 
<tr class="param">
<td><app:AppListEdit id="ledtPicklistValues" runat="server"/></td>
</tr>
 
 
</table>


The code in the EditAll.cs file is as follows:

using System;
using System.Web.UI;
using System.IO;
using System.Globalization;
using Microsoft.Crm;
using Microsoft.Crm.Application.Controls;
using Microsoft.Crm.Application.Forms;
using Microsoft.Crm.Application.Components.UI;
 
namespace HubKey.Crm.Web.Tools.SystemCustomization.Attributes
{
    public static class Globals
    {
        public static readonly string LOCID_EDIT_ALL_BUTTON_TEXT;
        public static readonly string LOCID_EDIT_ALL_DIALOG_TITLE;
        public static readonly string LOCID_EDIT_ALL_DIALOG_DESC;
        public static readonly string LOCID_EDIT_ALL_VALUE_LABEL_PAIRS;
        public static readonly string LOCID_EDIT_ONLY_ONE_DEFAULT_ALLOWED;
        public static readonly string EDIT_ALL_JS_PATH;
        public static readonly string LOCID_NUMBER_RANGE_VALUE_MASK;
        public static readonly string LOCID_VALUE_NOT_UNIQUE;
        public static readonly char DEFAULT_ITEM_CHAR;
 
        static Globals()
        {
            //TODO: Localize as needed.
            DEFAULT_ITEM_CHAR = '*';
            LOCID_EDIT_ALL_BUTTON_TEXT = "Edit All";
            LOCID_EDIT_ALL_VALUE_LABEL_PAIRS = "Value / Label Pairs";
            LOCID_EDIT_ALL_DIALOG_TITLE = "Modify List Values";
            LOCID_EDIT_ALL_DIALOG_DESC = "Modify all the values and labels in this picklist. Place each value / label pair on a new line separated by a carriage return. Separate values and labels with a tab. ";
            LOCID_EDIT_ALL_DIALOG_DESC += "If you don't include a value on each line (just the label) a value will be automatically created and incremented. ";
            LOCID_EDIT_ALL_DIALOG_DESC += "*** Warning: if any records use values that you are modifying, you must update those records to use the modified value before you do so here. ***";
            LOCID_EDIT_ONLY_ONE_DEFAULT_ALLOWED = "Only one value can be default. (Value = '{0}').";
            LOCID_NUMBER_RANGE_VALUE_MASK = "You must enter a whole number between {0} and {1}. (Value = '{2}').";
            LOCID_VALUE_NOT_UNIQUE = "Option values must be unique. The value {0} is already being used by option '{1}'.";
            EDIT_ALL_JS_PATH = "/_static/tools/systemcustomization/attributes/scripts/editall.js";
#if (DEBUG)
            EDIT_ALL_JS_PATH += "?" + Guid.NewGuid().ToString();
#endif
        }
    }
 
    public partial class EditAllPage : AppPage
    {
        protected TextArea txtLabel;
 
        protected override void ConfigureForm()
        {
            DialogForm currentForm = base.CurrentForm as DialogForm;
 
            currentForm.DialogTitle = Globals.LOCID_EDIT_ALL_DIALOG_TITLE;
            currentForm.DialogDescription = Globals.LOCID_EDIT_ALL_DIALOG_DESC;
        }
 
        protected override void ConfigurePage()
        {
            base.CurrentHeader.SetScriptFile(Globals.EDIT_ALL_JS_PATH);
            base.CurrentHeader.SetResource("LOCID_NUMBER_RANGE_VALUE_MASK", Globals.LOCID_NUMBER_RANGE_VALUE_MASK);
            base.CurrentHeader.SetResource("LOCID_VALUE_NOT_UNIQUE", Globals.LOCID_VALUE_NOT_UNIQUE);
            base.CurrentHeader.SetResource("LOCID_EDIT_ONLY_ONE_DEFAULT_ALLOWED", Globals.LOCID_EDIT_ONLY_ONE_DEFAULT_ALLOWED);
            base.CurrentHeader.SetResource("DEFAULT_ITEM_CHAR", Globals.DEFAULT_ITEM_CHAR.ToString());
        }
    }
 
    public class AppListEditAll : CrmUIControl
    {
        AppListEdit _listEdit;
 
        public AppListEditAll() { }
 
        protected override void ConfigureHeader()
        {
            base.ConfigureHeader();
            CrmUIControlBase.CurrentHeader.SetScriptFile(Globals.EDIT_ALL_JS_PATH);
            CrmUIControlBase.CurrentHeader.SetClientVar("_minValueForCustomPicklists", 1);
            CrmUIControlBase.CurrentHeader.SetClientVar("_maxValueForPicklists", 2147483646);
            CrmUIControlBase.CurrentHeader.SetClientVar("_minValueForSystemPicklists", 200000);
            CrmUIControlBase.CurrentHeader.SetResource("DEFAULT_ITEM_CHAR", Globals.DEFAULT_ITEM_CHAR.ToString());
 
            _listEdit = this.Page.FindControl("ledtPicklistValues") as AppListEdit;
            if (_listEdit != null)
            {
                this.Disabled = _listEdit.Disabled;
                this.ReadOnly = _listEdit.ReadOnly;
            }
        }
 
        protected override void Render(HtmlTextWriter output)
        {
            TextWriter innerWriter = output.InnerWriter;
            innerWriter.Write("<div id=\"{0}\" {1}>", CrmEncodeDecode.CrmHtmlAttributeEncode(this.ID), (this.ReadOnly || this.Disabled) ? "disabled" : string.Empty);
            innerWriter.Write("<table width=\"100%\" cellpadding=\"0\" cellspacing=\"0\"><tr><td>");
            innerWriter.Write("<tr class=\"listEdit_vspacer\"><td></td></tr>");
            innerWriter.Write("<table width=\"100%\" style=\"table-layout:fixed;\" cellspacing=\"0\" cellpadding=\"0\">");
            innerWriter.Write("<tr height=\"100%\"><td></td>");
            innerWriter.Write("<td class=\"listEdit_hspacer\"></td>");
            innerWriter.Write("<td class=\"listEdit_buttons\">");
            innerWriter.Write("<table width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">");
 
            this.RenderButton(output, "btnEditAll", (this.Disabled || this.ReadOnly)? "": "editAll();", Globals.LOCID_EDIT_ALL_BUTTON_TEXT);
 
            innerWriter.Write("</table></td></tr></table></td></tr></table></div>");
 
        }
 
        private void RenderButton(HtmlTextWriter output, string id, string onClick, string text)
        {
            TextWriter innerWriter = output.InnerWriter;
            innerWriter.Write("<tr><td nowrap style=\"padding-right:5px;\">");
            id = string.Format(CultureInfo.InvariantCulture, "{0}_{1}", CrmEncodeDecode.CrmHtmlAttributeEncode(this.ID), CrmEncodeDecode.CrmHtmlAttributeEncode(id));
            Button button = new Button(id, string.Empty, onClick, false, "listEdit_button");
            button.InnerHtml = "<span class=\"autoellipsis\">" + text + "</span>";
            button.Text = text;
            button.Width = 205;
            button.RenderControl(output);
        }
 
    }
}


The last piece is the client side script - editAll.js. Paste the following code into that file:

var _nDefault, _aHiddenValues, _aSystemValues, _iMin, _iMax, _nMax;
 
function cancel()
{
    window.close();
}
 
function applychanges()
{
    var oResult = new Object();
    try
    {
        oResult.sText = serializeValues(txtLabel.DataValue)
        window.returnValue = oResult;
 
        window.close();
    }
    catch (e)
    {
    }
 
}
 
function alertAndThrow(sMessage)
{
    alert(sMessage);
    throw new Error(101, sMessage);
}
 
function appendValues(oXmlDoc, oValues, aValues, bEditable)
{
    var sMessage;
    var iDefault = 0;
    var aDupVals = new Array();
    var aLabels = new Array();
 
    oValues.setAttribute("default", -2147483648);
 
    for (var i = 0; i < aValues.length; i+=2)
    {
        var oValue = oXmlDoc.createElement("value");
        var n;
 
        if (bEditable)
        {
            var s = Trim(aValues[i]);
            if (s.charAt(0) == DEFAULT_ITEM_CHAR)
            {
                s = s.substr(1);
                if (++iDefault > 1)
                    alertAndThrow(formatString(LOCID_EDIT_ONLY_ONE_DEFAULT_ALLOWED, s));
            }
 
            n = LocStringToInt(s);
            if (isNaN(n) || n > _iMax || (n < _iMin && !_aSystemValues.contains(n)))
                alertAndThrow(formatString(LOCID_NUMBER_RANGE_VALUE_MASK, AddFormatting(_iMin, 0), AddFormatting(_iMax, 0), s));
            if (iDefault == 1)
            {
                oValues.setAttribute("default", n);
                iDefault++;
            }
        }
        else
            n = Number(aValues[i]);
 
        var j = i + 1;
        var sLabel = (j >= aValues.length ? "" : Trim(aValues[j]));
        if (sLabel != "")
        {
            if (bEditable)
                _nMax = Math.max(_nMax, n);
 
            aDupVals.push(n);
            aLabels.push(sLabel);
            oValue.setAttribute("value", n);
            oValue.setAttribute("label", sLabel);
            oValue.setAttribute("editable", bEditable? "1" : "0");
            oValues.appendChild(oValue);
        }
    }
    var iDup = aDupVals.indexOfDuplicate();
    if (iDup > -1)
        alertAndThrow(formatString(LOCID_VALUE_NOT_UNIQUE, aDupVals[iDup], aLabels[iDup]));
}
 
function serializeValues(sXml)
{
    var oXmlDoc = CreateXmlDocument();
    var oValues = oXmlDoc.createElement("values");
    var rRegEx = /[\n|\t]/;
    var aValues = _aHiddenValues.join("").split(rRegEx);
    _nMax = 0;
 
    oXmlDoc.documentElement = oValues;
    appendValues(oXmlDoc, oValues, aValues, false);
    aValues = getTabbedValues(sXml);
    appendValues(oXmlDoc, oValues, aValues, true);
 
    if (++_nMax > _iMin)
        oValues.setAttribute("next", _nMax);
    else
        oValues.setAttribute("next", _iMin);
 
    return oValues.xml;
}
 
function getTabbedValues(sXml)
{
    var aValues = new Array();
    if (sXml == null)
        return aValues;
 
    var aLines = sXml.split(/\n/);
    var iVal = _iMin - 1;
    var iUserVal = iVal;
 
    for (var i = 0; i < aLines.length; i++)
    {
        var label = Trim(aLines[i]);
        var bDefault = false;
        if (label.charAt(0) == DEFAULT_ITEM_CHAR)
        {
            label = label.substr(1);
            bDefault = true;
        }
        var aPairs = label.split(/\t/);
        if (aPairs != null && aPairs.length > 1)
        {
            iVal = LocStringToInt(aPairs[0]);
            label = aPairs[1];
            iUserVal = Math.max(iVal, iUserVal);
        }
        else
            iVal = Math.max(++iUserVal, ++iVal);
        aValues.push((bDefault ? DEFAULT_ITEM_CHAR : "") + iVal);
        aValues.push(label);
    }
    return aValues;
}
 
function deserializeValues(oDataXml)
{
    var oValuesNode = oDataXml.documentElement;
    var iValueNode, oValueNode, oValueNodeList;
    var oValue, aTarget, oAttributes;
    var aValues = new Array();
    _aHiddenValues = new Array();
    _aSystemValues = new Array();
    oValueNodeList = oValuesNode.selectNodes("value");
 
    var oDefault = oValuesNode.attributes.getNamedItem("default");
    if (!IsNull(oDefault))
        _nDefault = Number(oDefault.value);
 
    for (iValueNode = 0; iValueNode < oValueNodeList.length; iValueNode++)
    {
        oValueNode = oValueNodeList.item(iValueNode);
        oAttributes = oValueNode.attributes;
        target = (oAttributes.getNamedItem("editable").value == '1' ? aValues : _aHiddenValues);
        var n = Number(oAttributes.getNamedItem("value").value);
        if (target == aValues && n < _iMin)
            _aSystemValues.push(n);
        if (target == aValues && n == _nDefault)
            target.push(DEFAULT_ITEM_CHAR);            
        target.push(n);
        target.push("\t");
        target.push(oAttributes.getNamedItem("label").value);
        target.push("\n");
    }
    var result = aValues.join("");
    return result;
}
 
function editAllWindowOnLoad()
{
    if (typeof(txtLabel) != "undefined")
    {
        Array.prototype.indexOfDuplicate = function() {
            var n = this.length;
            for (var i=0; i<n; i++)
                for (var j=i+1; j<n; j++)
                    if (this[i]==this[j]) return i;
            return -1;
        }
 
        Array.prototype.contains = function( value ) {
                var n = this.length;
                for (var i=0; i<n; i++)
                    if (this[i]==value) return true;
                return false;
        }
        var oArgs = window.dialogArguments;
 
        txtLabel.DataValue = oArgs.sText;
        _iMin = oArgs._iMin;
        _iMax = oArgs._iMax;
        _aHiddenValues = oArgs._aHiddenValues;
        _aSystemValues = new Array();
 
        for(var i=0; i<oArgs._aSystemValues.length; i++)
            _aSystemValues.push(oArgs._aSystemValues[i]);
 
        txtLabel.attachEvent('onkeydown', txtLabelOnKeyDown);
        txtLabel.SetFocus();
    }
}
 
function txtLabelOnKeyDown()
{
    if (event.keyCode == 9)
    {
        if (event.srcElement)
        {
            event.srcElement.selection = document.selection.createRange();
            event.srcElement.selection.text = "\t";
        }
        return false;
    }
    else
    {
        return true;
    }
}
 
window.attachEvent('onload', editAllWindowOnLoad);
 
function editAll()
{
    var oArgs, oResult;
    var oXmlDoc = CreateXmlDocument(false);
    oArgs = new Object();
    oXmlDoc.loadXML(ledtPicklistValues.DataXml);
 
    _iMin = ((_bIsCustomAttribute + "").toLowerCase() == "true" ? _minValueForCustomPicklists: _minValueForSystemPicklists);
    oArgs.sText = deserializeValues(oXmlDoc);
    oArgs._iMin = _iMin;
    oArgs._iMax = _maxValueForPicklists;
    oArgs._aHiddenValues = _aHiddenValues;
    oArgs._aSystemValues = _aSystemValues;
    oResult = openStdDlg(prependOrgName("/tools/systemcustomization/attributes/editAll.aspx"), oArgs, 500, 450);
 
    if (oResult == null)
    {
        return;
    }
    try
    {
        ledtPicklistValues.DataXml = oResult.sText;
    }
    catch(e)
    {
    }
}


The only thing left to do is find the public key token for your assembly. There are various ways to do this, but if you build your assembly now, the post-build events you copied in earlier will publish the assembly to the GAC. If you browse to the GAC folder after you build the solution with windows explorer and find the published assembly, (e.g. C:\WINDOWS\assembly\HubKey.Crm.Web.Tools.SystemCustomization.Attributes), you'll see the public key token there (you can copy this by viewing the properties of the assembly).

Back in Visual Studio there are two places you'll need to paste in the key token. Do a search and replace in all files for PUBLICKEYGOESHERE and replace that string with the token string.

Build the assembly once more to automatically copy these changes to the right file locations, and you should now see a functioning "Edit All" button on the edit attribute form when you choose picklist as the type from the dropdown. If the build fails because of the the post-build events, try building it one more time (there may be a locked file).

To back out the changes you've made, replace the modified manageAttribute.aspx page with the original page that you copied. Remove the assembly, editAll.js file, and editAll.aspx page from the file locations specified in the post-build events.

Hopefully this helps to save some time for your entity customizing users. If you have any questions or comments, please post them here.

Friday, January 9, 2009

Copy and Paste CRM Picklist Values - Part II

In Part I, I posted a screen capture demonstrating customization of the out-of-the-box edit attribute form in Microsoft Dynamics CRM 4.0. A custom "Edit All" button is displayed when a picklist type is selected which enables the editing of multiple picklist value / label pairs at once. The allows copying and pasting of lists from other data sources when setting up picklists - which can be a time saver - especially when setting up many, or large, picklists.

In this post I'm going to go over setting up the project that you'll need in order to enable this customization. I'm going to assume familiarity with Visual Studio 20005, C#, ASP.NET, and JavaScript. If you're not comfortable with these technologies, I would recommend either using existing workarounds, for example the online XML generator mentioned here, or hiring a CRM consulting firm like HubKey to assist you.

To begin, on your CRM development machine, in Visual Studio, create a new C# class library project and call it AttributesPicklistEditAll. You'll need to add references to the following four Microsoft Dynamics CRM DLLs from your server root folder (e.g. C:\Program Files\Microsoft Dynamics CRM Server) - all paths below will be relative to this root:

\Tools\Microsoft.Crm.dll
\CRMWeb\bin\Microsoft.Crm.Application.Components.Application.dll
\CRMWeb\bin\Microsoft.Crm.Application.Components.UI.dll
\CRMWeb\bin\Microsoft.Crm.Application.Pages.dll

Add a fifth reference to System.Web. (You won't be needing the reference to System.Xml or System.Data).

We're going to be making a change to the default manageAttribute.aspx page so that a new Edit All button is rendered for picklist types. Make a backup of that file, which is in the following location:

\CRMWeb\Tools\SystemCustomization\Attributes\manageAttribute.aspx

and then add the file as an existing item to your project in Visual Studio.

Once you've added it in Visual Studio, copy it and rename the copy to editAll.aspx
This will be the popup dialog that opens when the Edit All button is clicked. Open the renamed file (editAll.aspx) and remove all of the text.

Rename the default project file class1.cs to EditAll.cs

Add a new item to your project - a JScript file called editAll.js

In the project properties / signing tab, make sure that the assembly to be generated is signed by a strong name key file (e.g. key(.snk)). On the application tab, change the assembly name and default namespace to HubKey.Crm.Web.Tools.SystemCustomization.Attributes

On the project properties / build events tab add the following post-build event commands (assuming here that your server root is "C:\Program Files\Microsoft Dynamics CRM Server") - these commands reset IIS, make a copy of your original manageAttribute.aspx if you haven't already done so, copy (overwriting) the aspx pages and js file to the necessary locations, and then copy the assembly to the GAC:

call iisreset

if not exist "C:\Program Files\Microsoft Dynamics CRM Server\CRMWeb\Tools\SystemCustomization\Attributes\manageAttribute.aspx.orig" copy "C:\Program Files\Microsoft Dynamics CRM Server\CRMWeb\Tools\SystemCustomization\Attributes\manageAttribute.aspx" "C:\Program Files\Microsoft Dynamics CRM Server\CRMWeb\Tools\SystemCustomization\Attributes\manageAttribute.aspx.orig"

xcopy "$(ProjectDir)*.aspx" "C:\Program Files\Microsoft Dynamics CRM Server\CRMWeb\Tools\SystemCustomization\Attributes" /D /I /Y
xcopy "$(ProjectDir)*.js" "C:\Program Files\Microsoft Dynamics CRM Server\CRMWeb\_static\Tools\SystemCustomization\attributes\scripts" /D /I /Y

"C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe" /U "$(TargetPath)"
"C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\gacutil.exe" /F /I "$(TargetPath)"


You should end up with a solution window that looks like the following:

Long Running Job Status Page

In Part III I'll go over the code that you'll need to support this project.

Monday, January 5, 2009

Copy and Paste CRM Picklist Values - Part I

The web based GUI for MS CRM 4.0 offers essentially the same user interface to edit picklist attribute values as version 3. There's a tidy web form with all the buttons you'd expect in order to manage individual values (add, edit, delete, etc.), but if you want to add a range of values, say copy and paste them from an Excel spreadsheet, you'll be forced either to add them one-at-a-time, or to use one of the tools out there to generate xml to paste into the right place in your entity definition.

There is a very nice web based tool to help you do just that, but unfortunately anyone creating new entities or customizing existing ones will have to be comfortable manually editing those xml entity files - not something everyone may be happy to do.

The screen capture below demonstrates an alternative: customizing the edit attribute form to add an "edit all" button that allows multiple picklist value / label pairs to be edited and copied at once. In this example, I'm setting up an entity to manage NAICS codes. Rather than having to enter the 20 economic sector codes one-by-one, I can simply copy and paste the value / labels pairs from a spreadsheet.

In follow up posts to this one, I'll go over the details of how to set this up. This will involve creating a new aspx page and writing code to do the behind the scenes work.




Monday, December 22, 2008

Get Distinct Lookup Values

Here's another code example using HubKey's Client-side API for SharePoint. In this case 2 lists are created, a document library and a generic list. A lookup field (Subject) is added to the document library which looks up values in the Title column on the second list. The example also demonstrates getting distinct rows (in this case lookup values) by using the DataView.ToTable() method.

using System;
using System.Collections.Generic;
using System.Text;
// using HubKey's Remote SharePoint API instead of the native object model so we can update a remote server
using HubKey.Web.Services.SharePoint;
//using Microsoft.SharePoint;
using System.Data;
 
namespace DevHoleDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            //always use a test server and site with this beta API
            SPWeb web = new SPSite("http://remoteserver/testsite001/").OpenWeb();
 
            Guid subjectsListId = web.Lists.Add("HubKey Subjects", "A temporary list.", SPListTemplateType.GenericList);
            SPList subjectsList = web.Lists[subjectsListId];
 
            // note that using the SPList.Items property here with the HubKey API returns all the items in the list
            // which can be expensive if the list contains many items already. See the following code example for a 
            // better way to add items to a list specific to the HubKey API:
            // http://www.hubkey.com/Documents/HubKeySPAPI/html/60759825-9963-20e4-c31e-ae93ea608330.htm
            SPListItemCollection subjects = subjectsList.Items;
 
            SPListItem item1 = subjects.Add();
            item1["Title"] = "SharePoint";
            item1.Update();
 
            SPListItem item2 = subjects.Add();
            item2["Title"] = "Microsoft CRM";
            item2.Update();
 
            SPListItem item3 = subjects.Add();
            item3["Title"] = "Microsoft CRM";
            item3.Update();
 
            Guid booksDocLibId = web.Lists.Add("HubKey Books", "A temporary document library.", SPListTemplateType.DocumentLibrary);
            SPList booksDocLib = web.Lists[booksDocLibId];
 
            booksDocLib.Fields.AddLookup("Subject", subjectsListId, false);
            booksDocLib.Update();
            SPFieldLookup subjectLookup = new SPFieldLookup(booksDocLib.Fields, "Subject");
            subjectLookup.LookupField = "Title";
            subjectLookup.Update();
 
            // build the query to get a list of subjects
            // (pretend we are starting from scratch)
            SPFieldLookup subjectLookup2 = (SPFieldLookup)booksDocLib.Fields["Subject"];
            SPList subjectsList2 = web.Lists[new Guid(subjectLookup2.LookupList)];
 
            SPQuery query = new SPQuery();
            query.ViewFields = "<FieldRef Name='" + subjectLookup2.LookupField + "'/>";
 
            // here we use a data table so that we can get distinct titles (subjects)
            // if you're not woried about distinct values you could ignore this, but it's
            // also a convenient way to query the list items (e.g. see the RowFilter below)
            DataTable subjectsTable = subjectsList2.GetItems(query).GetDataTable();
 
            DataView dataView = new DataView(subjectsTable);
            DataTable groupedSubjectsTable = dataView.ToTable(true, "Title");
 
            SPFieldLookupValue lookupValue = null;
 
            // here we're looping through 2 distinct title rows ('SharePoint' and 'Microsoft CRM')
            foreach (DataRow row in groupedSubjectsTable.Rows)
            {
                string title = (string)row[0];
 
                // get the ID of the first row that matches the title
                dataView.RowFilter = "Title = '" + title + "'";
                int firstId = (int)dataView.ToTable(true, "ID").Rows[0][0];
 
                // create a new lookup value (we'll use the 2nd row's lookupValue below)
                lookupValue = new SPFieldLookupValue(string.Format("{0};#{1}", firstId, title));
            }
 
            SPFile file = booksDocLib.RootFolder.Files.Add("test.txt", Encoding.ASCII.GetBytes("A test document."));
 
            // update the subject with the 2nd lookup value;
            file.Item["Subject"] = lookupValue;
            file.Item.Update();
 
            // clean up - pause here if you want to view the results on the server.
            subjectsList.Delete();
            booksDocLib.Delete();
 
        }
    }
}

Wednesday, November 26, 2008

SharePoint API: A Client-side Object Model

One common issue (see question #2) that developers using Microsoft's SharePoint platform are faced with is that the SharePoint object model can only be used when the application is run on a server in the SharePoint farm.

The following is code that demonstrates using HubKey's Client-side API for SharePoint. This API wraps many of SharePoint Server 2007's Web Services (Lists Web Service, Webs Web Service, etc.) to provide a familiar object model that can be used on remote client machines without having to process XML requests and responses. In this case the code from the MSDN article How to: Upload a File to a SharePoint Site from a Local Folder was copied verbatim, and it compiles and runs successfully on a client computer, even though the SharePoint DLLs are not referenced or installed.

Update: You can download a demo copy of the API - click here for information.

Update II: The API now includes a demo application - see this post for details.



using System;
using System.IO;
// Using HubKey.Web.Services.SharePoint in place of Microsoft.SharePoint
using HubKey.Web.Services.SharePoint;
 
namespace HubKey.DevelopmentHole
{
    class Program
    {
        static void Main(string[] args)
        {
            Program program = new Program();
            program.RunDemo();
        }
 
        public void RunDemo()
        {
            string webUrl = "http://remoteserver";
            SPSite site = new SPSite(webUrl);
            SPWeb web = site.OpenWeb();
 
            //create a new document library
            Guid id = web.Lists.Add("Temp Doc Lib", "A temp document library.", SPListTemplateType.DocumentLibrary);
            SPList list = web.Lists[id];
 
            //add a field
            list.Fields.Add("Upload Source", SPFieldType.Text, false);
            list.Update();
 
            string fileUrl = webUrl + "/Temp Doc Lib/New Folder/test.txt";
            string localFileName = @"D:\Temp\test.txt";
 
            string localFileContents = "A temp file uploaded by HubKey's SharePoint client-side API.";
 
            File.WriteAllText(localFileName, localFileContents);
 
            //Upload the file using the API code from 'How to: Upload a File to a SharePoint Site from a Local Folder'
            //see http://msdn.microsoft.com/en-us/library/ms454491.aspx
            UploadFile(localFileName, fileUrl);
 
            //Check out
            SPFile file = web.GetFile(fileUrl);
            file.CheckOut();
 
            //update the title and new upload source field by using the file's SPListItem property...
            file.Item["Title"] = "A temp title";
            file.Item["Upload Source"] = "HubKey's SharePoint client-side API.";
            file.Item.Update();
 
            //Check in
            file.CheckIn("Checked in by HubKey's SharePoint client-side API");
 
            //Get the file contents
            string serverFileContents = web.GetFileAsString(fileUrl);
            System.Diagnostics.Debug.Assert(string.Equals(localFileContents, serverFileContents));
 
            //Tidy up
            list.Delete();
        }
 
        public void UploadFile(string srcUrl, string destUrl)
        {
            if (!File.Exists(srcUrl))
            {
                throw new ArgumentException(String.Format("{0} does not exist",
                    srcUrl), "srcUrl");
            }
 
            SPWeb site = new SPSite(destUrl).OpenWeb();
 
            FileStream fStream = File.OpenRead(srcUrl);
            byte[] contents = new byte[fStream.Length];
            fStream.Read(contents, 0, (int)fStream.Length);
            fStream.Close();
 
            EnsureParentFolder(site, destUrl);
            SPFile file = site.Files.Add(destUrl, contents);
            SPFolder folder = file.ParentFolder;
        }
 
        public string EnsureParentFolder(SPWeb parentSite, string destinUrl)
        {
            destinUrl = parentSite.GetFile(destinUrl).Url;
 
            int index = destinUrl.LastIndexOf("/");
            string parentFolderUrl = string.Empty;
 
            if (index > -1)
            {
                parentFolderUrl = destinUrl.Substring(0, index);
 
                SPFolder parentFolder
                    = parentSite.GetFolder(parentFolderUrl);
 
                if (!parentFolder.Exists)
                {
                    SPFolder currentFolder = parentSite.RootFolder;
 
                    foreach (string folder in parentFolderUrl.Split('/'))
                    {
                        currentFolder = currentFolder.SubFolders.Add(folder);
                    }
                }
            }
            return parentFolderUrl;
        }
    }
}