Request a topic or
contact an Arke consultant
404-812-3123
Arke Systems Blog | Useful technical and business information straight from Arke.

Arke Systems Blog

Useful technical and business information straight from Arke.

About the author

Author Name is someone.
E-mail me Send mail

Recent comments

Archive

Authors

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2024

CRM 4 to CRM 2011: SetFieldRequiredOrRecommended

The constant parameter for SetFieldRequiredOrRecommended has changed in CRM 2011.

CRM 4 CRM 2011
SetFieldRequiredOrRecommended(crmForm.all.new_notbillablereason_c, FORM_FIELD_TYPE_NORMAL, ""); SetFieldRequiredOrRecommended(crmForm.all.new_notbillablereason_c, FIELD_NOT_REQUIRED, "");
SetFieldRequiredOrRecommended(crmForm.all.new_notbillablereason_c, FORM_FIELD_TYPE_RECOMMENDED, ""); SetFieldRequiredOrRecommended(crmForm.all.new_notbillablereason_c, FIELD_RECOMMENDED, "");
SetFieldRequiredOrRecommended(crmForm.all.new_notbillablereason_c, FORM_FIELD_TYPE_REQUIRED, ""); SetFieldRequiredOrRecommended(crmForm.all.new_notbillablereason_c, FIELD_REQUIRED, "");

Posted by Eric Stoll on Wednesday, October 13, 2010 9:54 AM
Permalink | Comments (0) | Post RSSRSS comment feed

CRM 2011 - Living in Outlook

Right now, the Microsoft CRM Team is posting videos about CRM 2011.  The most recent one is Living in Outlook.  If you use Outlook and/or CRM at all, prepare to have your mind blown.  The level of integration between CRM 2011 and Outlook is absolutely amazing!  and the best thing is, since it uses Outlook's features to expose CRM data, as Outlook is updated, you get more features in CRM. Of course, that's a double-edged sword.  If you're struck on Outlook 2003, you're not going to get a lot of these cool features, as they disn't exist in Outlook 2003. Here is their full blog post as well: http://blogs.msdn.com/b/crm/archive/2010/10/04/microsoft-dynamics-crm-2011-living-in-outlook.aspx

 
 

Posted by Wayne Walton on Monday, October 4, 2010 4:27 PM
Permalink | Comments (0) | Post RSSRSS comment feed

Silverlight Issues

Silverlight often feels a bit much like a work in progress, other times just like parts weren’t thought through all the way.  I thought I’d post a few examples of the sorts of things we’ve run into while developing with it.  Main lesson is, somehow you will need to plan for the unplanned.

For example, in the global application error handler:

Exception tmp = e.ExceptionObject;
if (tmp != null)
{
    while (tmp.InnerException != null)
    {
        tmp = tmp.InnerException;
    }

    // Silverlight defect where File stream can error out if file open failed due 
    // to an open file error, then later the garbage collector tries to clean up
    // the stream.  Can't be reliably handled at regular code level because we don't
    // get back a stream to operate on due to the file method erroring out.
    if (tmp is InvalidOperationException)
    {
        // string parse is bad and fragile, but I can't find any way to get the error code
        // this should be error code -2146233079 per Reflector, but, well.
        if (tmp.Message.Contains("UI Thread") && tmp.Message.Contains("System.Windows.SaveFileStream.Dispose"))
        {
            // ignore
            return;
        }
    }
. . .

Forums suggest this one may have been fixed in 4.

My favorite issue is a layout issue: If you want a label next to a textbox, you put it in a horizontal stackpanel.  But horizontal stackpanels don’t constrain themselves horizontally.  Bottom line?  Your labels won’t wrap and will clip instead.   Solution?  Specify fixed widths on your labels.  I wonder if nobody did a form with labels next to textboxes and a dynamic layout all during the testing and design phase.

We also have these gems:

                // NOTE: A "Dialogs must be user initiated" error can crop up if running under the debugger.
                // It is a spurious error and will not happen when not using the debugger.
                // See http://forums.silverlight.net/forums/t/82454.aspx

 

// firefox doesn't render our silverlight right in an iframe.
function isfirefox() {
    if (/Firefox[\/\s](\d+\.\d+)/.test(navigator.userAgent)) {
        return true;
    } else {
        return false;
    }
}

 

/*
    This service has to be activated using basicHttp because Silverlight cannot 
    function against wsHttp.  
*/

 

        //sizechanged doesn't fire properly when column widths are changed, tabs need to
        // be hidden and refreshed for it to update... also, this can be fired
        // too early, before the column width has really changed. Layoutupdated seems to mostly work, though.

 

// assigning width directly is sub-optimal because
// it won't handle a situation where this column should be bigger
// than the main column... but the grid doesn't redraw properly
// if we don't.  Tried forcing just minwidth to work 
// with invalidatemeasure, invalidatearrange,
// measure, arrange, and updatelayout, all of which redraw the column
// header right but not the column data.

Categories: ASP.NET | Silverlight
Posted by David Eison on Sunday, September 26, 2010 5:59 PM
Permalink | Comments (0) | Post RSSRSS comment feed

Sitecore:Custom Error Pages

One of the important steps of any website is setting it up to fail Nicely. Sitecore has done a lot of this for us with their error pages, but that may not be the look you want your visitors to see! And what if Sitecore isn’t able to help at all? Not so pretty with ASP.NET error pages…

But do not fear! With the configuration files Sitecore makes available, this is a snap to set up and fix.***

***You Will need to change the web.config file, so please back it up before making any changes suggested here!

The first and hardest step – make your error/not found pages that you want your visitors to see – these can be html pages, aspx pages, or even an existing page within Sitecore.

Usually Sitecore will handle your errors nicely, but if .NET explodes you will need to display something to the user that will Not fail, so I’d VERY STRONGLY recommend an html page for errors and not aspx pages: if there’s an error in the error page, you’re back to square one.

Upload these files to your Website directory or a subfolder within it – for our example we have saved the files in Website/ErrorPages/.

Now for the magic: In the website folder is a folder called App_Config and inside that is the Include folder. This folder automagically updates the web.config with new settings – it doesn’t require anything to be restarted and immediately takes effect. Also – using these files instead of modifying the web.config directly means you can make web.config type changes in packages without completely destroying the site.

There should be a file in that directory called: SitecoreSettings.config.example

This is what we will be changing into SitecoreSettings.config and adding our custom ‘not found’ page details.

Currently the file looks like this, along with some comments describing the use of the file at the top:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<
sitecore>
<
settings>
<!--
REQUIRE LOCK BEFORE EDITING
If true, the user must have a lock on a document before
he can edit it, otherwise it is always ready for editing
-->
<
setting name="RequireLockBeforeEditing" value="false"/>

</
settings>
</
sitecore>
</
configuration>

We are going to erase this, and put in the following:

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<
sitecore>
<
settings>
<
setting name="ItemNotFoundUrl">
<
patch:attribute name="value">/ErrorPages/404.html</patch:attribute>
</
setting>
<
setting name="LinkItemNotFoundUrl">
<
patch:attribute name="value">/ErrorPages/404.html</patch:attribute>
</
setting>
<
setting name="LayoutNotFoundUrl">
<
patch:attribute name="value">/ErrorPages/404.html</patch:attribute>
</
setting>
<
setting name="ErrorPage">
<
patch:attribute name="value">/ErrorPages/Error.html</patch:attribute>
</
setting>
</
settings>
</
sitecore>
</
configuration>

The above basically tells the web.config file to update the specified settings with our new and improved values.

You can choose other files to use or have different files for each – and these special pages do not stop any logging from happening on the server – they simply give a better experience for your users when your site is having some trouble.

ItemNotFoundUrl and LinkItemNotFoundUrl will come up when a visitor tries to access an item that doesn’t exist or hasn’t been published (it doesn’t exist yet on the web database). This replaces the default value of: /sitecore/service/notfound.aspx with our own /ErrorPages/404.html

You’ll notice that the default is pulling from /sitecore/service ß if you have blocked this directory or don’t have it available, you will need to make the changes above so that users see Some kind of page instead of a browser default.

LayoutNotFoundUrl will come up when the user tries to visit an item that does exist, but no layouts have been assigned to the item so Sitecore doesn’t know what to display for the user. This replaces /sitecore/service/nolayout.aspx in the default web.config file.

ErrorPage should come up whenever there is a generic error within Sitecore – the default setting we are overwriting is: /sitecore/service/error.aspx

Save your new file without the .example extension and you should now see your custom error pages whenever you try to access missing items!

The next step is the change to the web.config for handling errors when sitecore can’t handle them for us – save a backup of your web.config and then open it up in an editor.

Here is what we are looking for:

<!--  CUSTOM ERROR MESSAGES
Set customError mode values to control the display of user-friendly
error messages to users instead of error details (including a stack trace):

"On" Always display custom (friendly) messages
"Off" Always display detailed ASP.NET error information.
"RemoteOnly" Display custom (friendly) messages only to users not running
on the local Web server. This setting is recommended for security purposes, so
that you do not display application detail information to remote clients.
-->
<
customErrors mode="On" />

Search for customErrors to find it quickly. It may also have one of the above settings, RemoteOnly or Off. I would recommend On or RemoteOnly if you won’t have visitors accessing the site locally.

We want to change the setting to also include a redirect to our new Error page:

<customErrors mode="RemoteOnly" defaultRedirect="/ErrorPage.htm"/>

Now, if anything serious should happen, your users will not end up seeing an ASP.NET error page telling them it is broken and will instead get to see a page of your choice! This should Not point to a page you have in Sitecore since it will not work in the case of ASP.NET failing.

Crossposted from Sitecore Adventures!


Posted by Amy Winburn on Thursday, September 16, 2010 9:11 PM
Permalink | Comments (0) | Post RSSRSS comment feed

Internet Explorer 9 Beta and Dynamics CRM

With the release of the Internet Explorer 9 Beta today, I just wanted to make sure that everyone knows not to install IE9 on any machine using CRM Online, CRM 4.0 or testing out the CRM 2011 Beta.  IE9 Beta is currently unsupported for all three CRM environments, and if the IE8 Beta is any indication, it will break things in a big way.

So enjoy testing the Internet Explorer 9 Beta, just don't do it on a machine that uses Dynamics CRM. 


Posted by Wayne Walton on Wednesday, September 15, 2010 12:24 PM
Permalink | Comments (0) | Post RSSRSS comment feed

Undoing Sql Server Database Engine Tuning Advisor

Trying out a set of recommendations from database engine tuning advisor on a test CRM system, I saw one particular query go from 1 second to 25 seconds.  Obviously I need to remove the recommendations and see if it’s a fluke and see if any other queries changed.  But there is no ‘unapply’ command. I can’t find anything better than rewriting the sql file or restoring from a backup.  Rewriting the sql file is easier since there are only two types of statements in it – create index and create statistics.  Regular expressions to the rescue…

First off, in tuning advisor, always always always pick ‘Save Recommendations’ before (or instead of) apply recommendations. This gives you a SQL file with all the intended changes.

Since it’s a CRM system I was only tuning by adding indexes and statistics.  Index creates are very easy to replace – change CREATE to DROP, get rid of all the extra info after the first line.  The hard part is finding a regular expression tool that can handle multi line replace – sql server and visual studio and notepad++ don’t appear to be up to the task.  I ended up using this visual studio add-in which adds a proper regex parser to visual studio, but it has some bugs so I wouldn’t rely on it.  Make one find/replace, save, reload the file, make the second find/replace, save the file, then put the tool away. 

I just want to find:

^CREATE .*?(INDEX.*)$[^$]*?$?(go)

and replace:

DROP $1

go

regex[7]

 

Statistic drop syntax is slightly different than create syntax.  But not multi-line, so a bit easier, just needs groups.

Find:

CREATE STATISTICS (.*) ON (.*)\(.*\)

Replace With:

DROP STATISTICS $2.$1

And now I have a SQL file that undoes the changes my tuning advisor made.


Categories: SQL Server
Posted by David Eison on Tuesday, September 14, 2010 6:55 PM
Permalink | Comments (0) | Post RSSRSS comment feed

ASP.NET click tracking

In order to integrate some classic ASP pages with an ASP.NET site, I need the user to go through an intermediate session copy page every time they click on a link to an asp page. 

This requirement is also very common in ‘click tracking’, where you want to keep track of what outgoing links users have clicked on from your site.  You do this by sending them through an intermediate page before redirecting them out of the site.

This leads to a lot of ugly links like:

http://demo/util/sessionupdate.aspx?url=%2targetdir%2findex.asp

When previously they were clear links like:

http://demo/targetdir/index.asp

How can we get back our clear links while still going through the intermediate page every time?

Before clicking:

image

After clicking:

image

One of the earliest approaches was to use javascript to change the status bar.  This led to a lot of fake urls so it was widely disabled by the browser makers – IE6 is the last major browser where this approach still works. 

Another approach is to use javascript ‘onclick’ to redirect the user; onclick runs before the href is followed, and if you return false from it the href won’t be followed, and you can use a javascript redirect like window.location.  This tends to work, but only for users with javascript enabled, and I believe there are some instances where it doesn’t really work.  In my specific case, I need to always send the user through the intermediate page.  In general, it’s cleaner to send the user only where the href actually links them.

Search engines like Google, Yahoo and Bing are all currently using a javascript ‘onmousedown’ event to respond to the users clicks.  Yahoo and Google both serve up the regular href, so that your status bar has the nice clear link, but when you mouse down they swap it out with their click tracking page.  (Bing instead kicks off an AJAX xmlhttprequest as soon as you mousedown.) 

Before clicking:

image

After mouse down:

image

These approaches have the advantage of serving links up in a fail-safe works-fine-with-javascript-off manner, and also only tracking real clicks.  However, in my specific case, my fail-safe is the opposite: I need to go through the intermediate page if javascript is off.

So, my solution is to serve up the links with the intermediate page.  Then if the user has javascript enabled I use javascript to replace the intermediate link with the clear direct link, and in onmousedown I swap it back again replacing the clear direct link again with the intermediate link.  End result is everybody should get gets session links, and users with javascript see nice clear links in their status bars.

Below is an asp.net user control that makes these click tracking links.

Register controls in web.config:

    <pages buffer="true" validateRequest="true">
      <controls>
        <add tagPrefix="uc" namespace="arkelibrary.controls" assembly="arkelibrary"/>
      </controls>
    </pages>

Using it on a page:

    <!-- specialchars are: ?&<z>![test]+ -->
    <fieldset>
    <legend>Click Tracking with JavaScript</legend>
    <uc:asplink runat="server" href="/webtest/asppage.asp?param1=foo&param2=specialchars%3f%26%3cz%3e!%5btest%5d%2b">ASP Page A - manually encoded url</uc:asplink>
    <br />
    <uc:asplink ID="Asplink1" runat="server" >ASP Page B - URL encoded in codebehind</uc:asplink>
    <br />
    </fieldset>
    <br />
    <fieldset>
    <legend>For comparison, same links as static HTML</legend>
    <a href="/webtest/asppage.asp?param1=foo&param2=specialchars%3f%26%3cz%3e!%5btest%5d%2b">Plain HTML Href - Direct</a>
    <br />
    <a href="http://demo/webtest/sessionupdate.aspx?url=/webtest/asppage.asp?param1=foo&param2=specialchars%3f%26%3cz%3e!%5btest%5d%2b">Plain HTML Href - Through intermediate session page</a>
    <br />
    </fieldset>

 

Manually setting a property in code behind for ASP Page B:

protected void Page_Init(object sender, EventArgs e)
{
    Asplink1.href = "/webtest/asppage.asp?param1=foo&param2=" + Server.UrlEncode("specialchars?&<z>![test]+");
}

 

asplink server control:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace arkelibrary.controls
{
    /// <summary>
    /// This control takes a href parameter and renders the page
    /// with a link to 'localclicktracker' and that href appended,
    /// plus javascript to swap the localclicktracker href out to the direct
    /// link and more javascript to swap it back in mouse down.  The purpose
    /// is to have clear direct links in the user's status bar, while
    /// always sending users through the clicktracker intermediate page regardless
    /// of whether they have javascript enabled or not.
    /// e.g.: 
    /// <uc:asplink runat="server" href="/dir/target.asp">ASP Page</uc:asplink>
    /// </summary>
    [DefaultProperty("Text")]
    [ToolboxData("<{0}:asplink runat=server Target='{1}'></{0}:asplink>")]
    [ParseChildren(true, "Text")]
    public class asplink : ArkeControlBase
    {
        private const string localclicktracker = "/webtest/sessionupdate.aspx?url=";

        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("")]
        [Localizable(true)]
        public string href
        {
            get
            {
                String s = (String)ViewState["href"];
                return ((s == null) ? String.Empty : s);
            }

            set
            {
                ViewState["href"] = value;
            }
        }

        [Bindable(true)]
        [Category("Appearance")]
        [DefaultValue("")]
        [Localizable(true)]
        public string Text
        {
            get
            {
                String s = (String)ViewState["Text"];
                return ((s == null) ? String.Empty : s);
            }

            set
            {
                ViewState["Text"] = value;
            }
        }

        protected override HtmlTextWriterTag TagKey
        {
            get
            {
                return HtmlTextWriterTag.A;
            }
        }

        protected override void AddControlAttributes()
        {
            AddUnencodedAttribute(HtmlTextWriterAttribute.Href, localclicktracker + href);
            base.Attributes.Add(HtmlTextWriterAttribute.Id.ToString(), this.ClientID);
        }

        protected override void OnPreRender(EventArgs e)
        {
            base.OnPreRender(e);
            Type type = GetType();
            if (!this.Page.ClientScript.IsClientScriptBlockRegistered(type, "friendlyhref"))
            {
                string script = @"<script type=""text/javascript"">
window.rewrite=function(t){
  try {
    var orig=t.href;
    var esc=encodeURIComponent||escape;
    if (t.href.indexOf(""" + localclicktracker + @""") < 0) {
      t.href=""" + localclicktracker + @""" + esc(t.href)
    }
    t.onmousedown="""";
  } catch (err) {try {t.href=orig;} catch (e2) {}}
  return true;
}
window.friendlyhref=function(id,target){ 
  try {
    var t=document.getElementById(id);
    if (t) {
      var orig=t.href;
      t.href=target;
      t.onmousedown=function() {return rewrite(t);};
    }
  } catch (err) {try {t.href=orig;t.onmousedown="""";} catch (e2) {}}
}
</script>";
                this.Page.ClientScript.RegisterClientScriptBlock(type, "friendlyhref", script);
            }
        }

        protected override void Render(HtmlTextWriter writer)
        {
            AddAttributesToRender(writer);
            writer.RenderBeginTag(HtmlTextWriterTag.A);
            if (Text == String.Empty)
            {
                Text = href;
            }
            writer.WriteEncodedText(Text);
            writer.RenderEndTag();

            // Render is too late in the page processing cycle for Page.ClientScript.RegisterClientScriptBlock
            // to work.  I could instead use a startup script registered in onprerender, but that 
            // would wait until the whole page was loaded before fixing up the links.  So
            // instead I just write javascript here directly so that it will run as soon as possible.
            string scriptForThis = String.Format(@"<script type=""text/javascript"">friendlyhref('{0}','{1}');</script>",
                this.ClientID, href);
            writer.WriteLine(scriptForThis);
        }
    }
}

 

Base class:

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace arkelibrary.controls
{
    /// <summary>
    /// WebControl by default will encode any attributes
    /// added to it.  We want to add two features:
    /// 1) Support already-encoded attributes
    /// 2) Support asserting that our attributes don't include any <%= %> blocks, which
    ///    show up during conversion from ASP and are easy to miss.
    /// </summary>
    public abstract class ArkeControlBase : WebControl
    {
        // we have to support two separate attribute collections, because
        // the base class collection encodes everything
        private Dictionary<HtmlTextWriterAttribute, string> unencodedAttrs = new Dictionary<HtmlTextWriterAttribute, string>();

        // code blocks can show up when translating from asp.
        // they are dangerous because they will work fine if in a plain html control,
        // but will fail if in a server control.  So, error out if we hit one.
        protected void AssertNoCodeBlocks()
        {
            foreach (var key in this.Attributes.Keys)
            {
                AssertNoCodeBlock(key, this.Attributes[key.ToString()]);
            }
            foreach (var pair in unencodedAttrs)
            {
                AssertNoCodeBlock(pair.Key, pair.Value);
            }
        }

        protected void AssertNoCodeBlock(object key, string value)
        {
            if (value.Contains("<%"))
            {
                throw new ArgumentException("Can not have asp.net block tag in control attribute: " + key + "=" + value);
            }
        }

        // override this method in subclasses and add attributes -
        // add to base.Attributes.Add(key,value) anything that should be encoded,
        // and unencodedAttrs[key]=value anything that should not be encoded.
        protected abstract void AddControlAttributes();

        protected void AddUnencodedAttribute(HtmlTextWriterAttribute key, string value)
        {
            unencodedAttrs[key]=value;
        }

        // calls our internal addcontrolattributes method, asserts we have no code blocks,
        // adds base attributes to writer, then adds our unencoded attributes to writer.
        protected override void AddAttributesToRender(
            HtmlTextWriter writer)
        {
            // add attributes to control
            AddControlAttributes();

            // validate attributes
            AssertNoCodeBlocks();

            // copy control attributes to writer
            base.AddAttributesToRender(writer);
            foreach (var pair in unencodedAttrs)
            {
                writer.AddAttribute(pair.Key, pair.Value);
            }
        }
    }
}

Posted by David Eison on Friday, September 3, 2010 7:35 PM
Permalink | Comments (0) | Post RSSRSS comment feed

CRM and external .js

As near as I can tell, if document.write is added to a CRM onload method the entire method is disabled and not served to the client.

document.write is usually a bad idea, but it is quite useful when including an external javascript page, because it blocks while waiting for the page to load. 

In CRM you have two choices for including external files:

  • Directly manipulate the DOM, which does an async load, which means your scripts might not load in order.  If you load jquery first and then something that depends on jquery, the something that depends on jquery might fail.
  • Or, you can use window.execScript, which is IE specific but synchronous.  See http://danielcai.blogspot.com/2010/02/another-talk-about-referencing-external.html – but add a ?version=whatever or ?version=Math.random parameter to give you some control over caching as versions change.

(Disclaimer: including external pages in CRM can lead to problems with the outlook client working offline – you need to include them in your /isv folder and distribute them via a separate installer to your users for them to continue to have offline functionality.  Javascript inside the entity is automatically synced by CRM to offline clients, but files in /isv aren’t.)

You could also take a look at CrmExt, which is a fully featured toolkit for handling these sorts of issues: http://blogs.msdn.com/b/kbowling/archive/2010/04/30/crmext-js-an-external-javascript-loader-for-dynamics-crm.aspx


Posted by David Eison on Wednesday, September 1, 2010 4:06 PM
Permalink | Comments (0) | Post RSSRSS comment feed

CRM and prepended org names

Customers with on-premise or installations of CRM will be familiar with seeing /ORGNAME/ as the root directory in their CRM URLs. 

Microsoft added this feature so that one CRM installation could support multiple different organizations (Internet Facing Deployments and CRM online have something similar but use different server names instead of the virtual directory so that cookies can be kept separate).

However, it can lead to some confusion when wanting to link to CRM files.  Say you put a .html file in an iframe – do you use orgname or not?

The /ORGNAME/ handling is done via an ASP.NET virtual path provider – which means that it only kicks in if the ASP.NET engine is processing the request.  By default, IIS is normally configured to process requests for URLs ending in .aspx via a “Handler Mapping” .aspx to the ASP.NET engine, but resources like .html or .js or .gif or .jpg usually get processed by the default Static File handler.

So a .html file under http://[crm server]/ORGNAME/isv/myapp/myapp.html will 404, but the same file as a .aspx would happily show up.  To get to the static file, you need to leave out the /ORGNAME/: http://[crm server]/isv/myapp/myapp.html

Note that if you do want to link to a .aspx, the global JavaScript function prependOrgName is the best way to add the org name:

prependOrgName(‘/isv/myapp/myapp.aspx’)


Posted by David Eison on Wednesday, September 1, 2010 3:32 PM
Permalink | Comments (0) | Post RSSRSS comment feed

How to do useful things with CRM: Put a button on a page

Sometimes it’s hard to tell where to start with CRM.  So, a goal oriented example: Lets add a button to do something on a CRM form.

First, a plug for two things:

  1. The .chm file in the sdk is fantastic. Read it. Ditto the examples in the example directories.
  2. Developing ISV Applications using Microsoft Dynamics CRM 4.0 has some high level concept overview info when you have some time to read.  It can help you to know what different concepts exist. 

On to our task:

Step 1: Make a decision.  We actually have four different ways we can add a button on a CRM form.  Yes, 4.  Here they are:

  1. Add an iframe pointing to a page under /ISV.  /ISV is a folder MS has designated for Independent Software Vendors to add new pages to CRM.
    1. Advantages:
      • Lets you use all of your normal C#/ASP.NET tools.
      • User is already logged in to the CRM website, so no worry about authentication.
      • Probably the easiest for a C# developer to understand.
    2. Disadvantages:
      • It’s an iframe. Iframes can behave weird, particularly with regard to sizing. They have to be square.
      • Offline Outlook client does not automatically sync files under /ISV.  If you need to support offline users with features backed by /ISV pages, you need to deal with making and distrubing a separate installer package for your pages.
      • Iframe loads separately from your main page.
      • Separately addressable via url, a user can bookmark just your iframe.
      • Might not be an option depending on your hosting.
  2. Add an iframe pointing to a page running in a separate web app
    1. Advantages:
      • A separate site can be easier to configure and set up, you get your own web.config file.  (it’s best to avoid relying on putting things in CRM’s web.config)
      • A separate site can have separate access control, such as be part of a public facing website.
      • Always an option.
    2. Disadvantages:
      • Same as #1, plus:
      • Depending on what your page will be doing, you may have to deal with user authentication in some way.
      • “Same domain security model” – you can cause headaches for yourself if you use a separate webserver.  By default javascript can only talk back to the server it was served from.  If you want to have javascript talk to the CRM server, that javascript needs to be served from the CRM server.   If you want to have javascript talk to a different server, that javascript needs to be served from the other server.  (via a script src= tag).  So you can end up with needing to logically split your code between the CRM form and a separate .js file on a different server, and it can get hard to keep straight.
  3. Add a button to the menu along the top of a CRM form.  This is done by editing isv.config, which is a configuration file MicroSoft provides for you to customize some aspects of CRM appearance and behavior.  You can add a new button in this file, and have it call a snippet of Javascript, and it will automatically show up on your page.
    1. Advantages:
      • Consistent interface, lots of things in CRM work via buttons on the menu at the top of the form.
      • Offline Outlook clients sync up the isv.config file, so you can deal with them consistently this way.
      • Always an option.
    2. Disadvantages:
      • Few choices – you get a button added to the right of the current menu.
      • If you add too many items on the menu, the labels will be removed to make room.
      • Sometimes it isn’t obvious to a new user that they should look up there.  Sometimes you really want a button somewhere in particular in the form.
  4. Add a button via javascript somewhere in the page.  This is done by adding javascript to the form’s onload and directly adjusting the DOM. 
    1. Advantages:
      • You can put the button right where you want in the page.
      • Offline Outlook clients sync up with javascript on the form.
      • Always an option.
    2. Disadvantages:
      • Working in JS directly on the form is hard to test.  Lots of save/publish/reload/wait.
      • JS directly on the form is stored in the DB.  Hard to version control.
      • Relying on the exact layout of things in the DOM is generally unsupported and likely to break one day with an MS upgrade.
      • (You can work around both of these problems: I use a variant of Joris Kalz’s JScript Export Tool to dump jscript to the filesystem where I then check it into revision control.  And Stunnware has a JavaScript Factory that looks great but I haven’t tried it yet.

Ok. Now, knowing a bit about what we’re getting into, we’re ready to start. 

For now, we’re picking option #1, an ISV page, because it’s very very straightforward and probably has the fewest gotchas.

Things to know about an ISV page:

  • /ISV is basically the only spot in CRM that MicroSoft has said that you can put things and they won’t be touched.

Originally you had to put your dlls in the main /bin or in the GAC, but very early on MS fixed it so that you could use /ISV/[yourapp]/bin.  You might still find some old pages saying you have to GAC it or use the main /bin while you are searching.

So, starting with a simple example, setting up.

Life will be easiest if you have a copy of CRM that you can work directly on.  Lacking that, make good backups, and then life will be second easiest if you can at least get filesystem access, say via a mounted drive.

First, start a download of crm sdk.  You’ll want it handy for the help file and examples, they are quite good, and often better than what you’ll find on googling.  I google it every time in case a  new one has come out since I last started a project. 

Now, a new web application project in visual studio.  This will hold our ASP.NET page that will end up in an IFrame when we are done:

tempsnagitfile

Lets do some plumbing:

Add references to the CRM Sdk.  There are three ways to do this.  (MSDN on this topic)  My opinion:

  1. Use the web services by adding a web reference and pointing it to CRM’s wsdl files. 
    1. Advantages:
      • You get static typed classes including all of your customized classes and customized attributes.  (I am strongly on the side of this being a fantastically good thing. Others don’t agree.)
    2. Disadvantages:
      • If you work with plugins, you’re not supposed to use this approach, so you’ll need to learn a separate way to work with DynamicEntity.
      • CRM WSDL uses weird wrapper types like CrmBoolean and have tricky gotchas like you can’t just null things by setting them to null.
  2. Take the CRM SDK dlls from the SDK you downloaded and add them as references.
    1. Advantages:
      • This is what you’ll see for plugins, so if you do it for your webpages too you’ll be used to the syntax.
      • Your code will be more flexible and easier to deploy on new CRM instances with different customizations.  If you want to write a generic tool, don’t pick choice 1.
    2. Disadvantages:
      • You don’t get to use the static typed classes unless you’re willing to limit yourself to the built-in non-customized versions. (Avoid these like the plague because if you try to mix them together you can force it to compile via casting foo, but good luck making it actually work.  If using the CRM SDK dlls, I always stick 100% to using just DynamicEntity.)
      • DynamicEntity is essentially a glorified hashtable with some annoyingly confusing tricks to it.  The compiler can’t catch typos in hashtable key strings for you, so you’ll be relying on more tricks like calling ToString() on enum values.
      • Your code will take longer to write and debug than option #1 or #3.
  3. Feed your CRM WSDL to the latest and greatest CRM SDK tool to get strongly typed entities that are friendlier than method #1 – the weird wrapper types are gone, and you can use LINQ.  Last I checked, however, this still wasn’t meant for plugins, so you still need dynamic entity if you work with plugins.  Personally, I have clients with a lot of plugins, so I haven’t spoiled myself with this API yet because I don’t want to get too used to it.

We’re going to start with option #1, because it’s reasonably quick to develop but we still learn a good bit of CRM guts.  Also we will be able to understand all of the current example code we find on the internet in blogs and in the sdk itself.  The new CRM SDK is still too new to have many good examples available.  Make a promise to yourself now that you’ll learn it in a few months.

There are two web services. One is “CrmSdk”, the other is “MetadataSdk”.  We’ll ignore metadata for now; just know that if you need to learn what values are valid for a given entity, or what values you can choose for a picklist, the metadata is where you would look.  Often if you are doing a client-specific custom app you can completely ignore it; if you are doing a more general purpose app it will be utterly essential.

(See “Accessing the Server Using the Web Service” in the help file.)

(Note: Some versions of Visual Studio have hidden ‘web references’ as an extra link you click under service references. You want to add a web reference.)

temp

CRM publishes its WSDL at http://<servername>[:<port>]/mscrmservices/2007/crmservice.asmx

By convention, name it CrmSdk:

t

Next up, grab the helper classes from the CRM SDK and add them to your project.  They add a bunch of useful utility methods to the CrmSdk for you.  The directions might be out of date.

t[6]

drag_from_sdk_to_solution

The namespace doesn’t match and needs to be fixed up.  If you take a look at your CrmSdk Web Reference, you’ll see it has namespace projectname.WhateverYouTypedOnAddWhichShouldBeCrmSdk.  Search-and-replace on helpers to match that.

namespace

 search_and_replace_namespace

You now have the most basic skeleton for talking to CRM.  You can build to be sure you got it right so far:

build_succeeded

Now we need a web page:

add_new_item

new_web_form

First things first, lets add the Assembly we were told to add back at the very beginning to point to our dll in the bin directory (from “Custom Code Best Practices” page). It needs to be the first directive, even before page.

<%@ Assembly Name="arkecrmexample.dll" %>

 

 

You can drag your example button from the toolbox or manually type it in. I usually manually type the main asp:button id= runat= part, then switch to designer to get properties to add the onclick because I forget the exact parameter types sometimes.  If you doubleclick on the event in the properties:

clickproperty

You get in your codebehind :

protected void btnExample_Click(object sender, EventArgs e)
{

}

Now I want to do something.

Crm Sdk has an example of a utility for getting a reference to the Crm Server in sdk\server\reference\cs\crmserviceutility.cs , but it’s unfortunately rather limited.  We’ll still start with it as the basis for making our own utility class, just like everybody else who has ever started working in Crm. 

Drag a copy to your helpers folder, change the namespace on it, and drop the using CrmSdk at the top since it’s in the right package now.  The two Metadata methods won’t compile unless you add metadata sdk; I comment them out for now.

crmserviceutility

using System.Text;
/* removed using CrmSdk and Metadata Sdk statements */

namespace arkecrmexample.CrmSdk /* used to be Microsoft.Crm.Sdk.Utility*/
{
    public class CrmServiceUtility

Now, lets make our button talk to CRM for something. We’ll send a simple ‘WhoAmI’ request.

Because this is a blocking service call it probably should be done as an async call, but we’ll do it the quick way for now and do a future blog post about changing it to an async page.

Add a using statement for our CrmSdk:

using arkecrmexample.CrmSdk;

See sdk\server\reference\cs\misc\whoami.cs and snag its code, put it into our button click for now.  Remove the MS namespace on your CrmServiceUtility call.

/* used to be Microsoft.Crm.Sdk.Utility.CrmServiceUtility*/
CrmService service = CrmServiceUtility.GetCrmService(crmServerUrl, orgName);
service.PreAuthenticate = true;


// Create the request object.
WhoAmIRequest userRequest = new WhoAmIRequest();

// Execute the request.
WhoAmIResponse user = (WhoAmIResponse)service.Execute(userRequest);

Now we need a server url and an org name.

Note that CRM servers often have two URLs, an internal (“Active Directory” authentication) and an external (“Internet Facing Deployment”, or IFD, which authenticates using asp.net web forms).  There is also different authentication for ‘partner hosted’ and ‘microsoft hosted’

We are going to punt on this as a topic for later and just look at the Active Directory authentication for now.  There are examples in the sdk of the other hosting types.

The easiest way to check your orgName is to load CRM and grab the first directory name.

I am evilly hard-coding these things for now because configuration is very slightly harder than web.config.  We’ll change this to a config file in a future blog post.  For now:

            string crmServerUrl = "arkecrm01";
            string orgName = "ArkeCRM";

Finally, we want to do something with the result. Stock asp.net, I add a literal and set its text.

ASPX:

    <div><asp:Literal ID="output" runat="server" Mode="Encode" /></div>

Code Behind, I added:

if (user != null)
{
    success = true;
    // this tostring is ugly, see http://blog.arkesystems.com/post/2010/08/Microsoft-Dynamics-CRM-40-ndash3b-Guids-ToString-ndash3b-Even-simple-things-can-be-complicated.aspx 
    // for a nice extension method for it.
    output.Text = "User Id is " + user.UserId.ToString("B", System.Globalization.CultureInfo.InvariantCulture).ToUpper(System.Globalization.CultureInfo.InvariantCulture);

}

 

Now, we’re almost done.  A brief word on authentication, then on to putting the page on our server.

The CRM helper method we used authenticated to CRM using ‘System.Net.CredentialCache.DefaultCredentials’, which means it told the CRM SDK that it is whoever is running the code. 

CRM web servers run with identity impersonate="true" in their web.config, which means that the person who is running the code will automatically appear to be whoever is viewing the CRM website.

That is fine for the specific situation we are in here - /ISV page on the CRM server, active directory install.  If we were doing the page outside of /ISV on a separate web server, or dealing with something like Internet Facing Deployment, we would net to get fancier on identifying the user.  One thing you can do for some calls is just always make the call as a particular crm user.  Another thing is CRM has an “Impersonate” feature, where you log in to the server as an admin user but then impersonate the actual user performing an action.  Search the help file for ‘Impersonation’ when you find yourself in a more complicated situation.

Ok.  Time to compile and put this on our web server.

CRM usually runs at c:\inetpub\wwwroot.  If you aren’t sure where it is, take a look at the IIS config on the web server.  If you have multiple CRM web servers, you’ll want to deploy your code to each of them.

We want to make a subdirectory under /ISV.

This subdirectory will contain our aspx and a bin folder.

The bin folder will contain our dll.

Important note: I am *not* copying my web.config.  web.config causes headaches under isv.  If you do copy it, you’ll have to remove some things.  In particular the default

<authentication mode="Windows" />

will cause trouble.  But just avoid your web.config for now.  You’re running under CRM, you have its web.config.  We’ll do application settings in a separate config file somewhere else soon.

So, I’m manually publishing these files for now. Later we’ll look at post-build steps to automate.  I could also just straight copy these files instead of using publish.

I opened up explorer, found my ISV directory, and made a directory for my app. 

Now back in visual studio I *deleted my web config entirely* – it’s trouble and I don’t want it there.  Now I right click on the project and pick publish.

The most important thing here is to triple check that path and be absolutely sure that I am publishing to a ISV\directory and not to the root.  Seriously.  Triple check.  Then go back up CRM.  Then check again.

publish

After checking, again, I publish.

Explorer looks good, just what I expected:

explorer_after_publish

Now I can pull this file up on the server as a quick sanity check.

There are two ways to pull up a URL under CRM:

1) Include org name – goes through CRM virtual path handler.

2) Leave out org name – goes through IIS at a lower level.

#1 is usually what you want, even if it gives you trouble at first, the trouble is usually because you missed a detail.

page

Hooray I have a button!  PUSH BUTTAN!

Would you believe that worked on my first try?

pushbuttan

Take a quick break to pretty things up a bit, snag the style sheets off a view source on the main page.

<link rel="stylesheet" type="text/css" href="/ArkeCRM/_common/styles/global.css.aspx?lcid=1033" />
<link rel="stylesheet" type="text/css" href="/ArkeCRM/_common/styles/global-styles.css.aspx?lcid=1033" />
<link rel="stylesheet" type="text/css" href="/ArkeCRM/_common/styles/global-dynamic-styles.css.aspx?lcid=1033" />
<link rel="stylesheet" type="text/css" href="/ArkeCRM/_common/styles/fonts.aspx?lcid=1033" />    

styled

Publish again or rebuild and recopy your dll to pick up the changes.  

Ok. Now to add my useless button to an iframe on an entity.  Grab an entity to work on.  I’ll make a new one.

newentity

I name my entity, turned off note and activities and added it to the settings tab.  I kept ownership at user because it will be useful for some examples later, but in general if the entity doesn’t involve records being owned by people I would flip it to Organization.

newentity[5]

Save, then add some attributes.  Your attributes will have a different prefix, the default is “new_”.

attributes

Edit the form and add some stuff to it.

Most importantly, add an IFRAME!

If you pick ‘pass record object-type code and unique identifier as parameters’, your URL will have parameters passed into it about the record the form is displaying.  This will be useful later, so I check it.

There is no need to ‘restrict cross-frame scripting’ unless you don’t trust your target frame.  If linking to some 3rd party site, keep it checked.  If it’s your own page, I believe you should be ok unchecking it, it will cause some javascript to not work someday.  Note also you have various formatting options. 

(Dependencies is strictly a protect-users-from-themselves features; if you add a dependency, the dependent field will not be allowed to be removed from the form in the form editor.  That’s all it does.)

add_iframe

Ok, save and close, and publish.

Go make a new one of these to check out your form. (You have to reload the main CRM page before the left hand menu picks up the new options after you make a new entity, so hit f5 on the main window then go to settings.)

 

example 

 

Taking a quick break to go over one really common situation:

Often you don’t know what to set the iframe URL to when the form is first loaded.  Or perhaps you don’t want to display it if you have a create form, you only display the iframe on an update form.  The solution here is to hard-code the url to /_root/Blank.aspx , then change it in javascript in the onload.

You’ll find lots of references on the internet telling you to set it to about:blank.  This can cause problems in odd cases, such as security warnings.  Ignore the about:blank advice and use /_root/Blank.aspx

Now, a quick review of some shortcuts that I took that we’ll want to fix in the near future.  I try not to take shortcuts, but this is long enough as is.  Still, it’s important you know about them:

  1. I hard-coded the org name in various places.  I should build up org name strings instead of hard-coding it.
  2. I didn’t use an external config file to grab my connection to CRM.
  3. I only dealt with one type of CRM installation, the on-premise active directory setup.
  4. My onclick method should call some helper functions – connecting to crm and whoami should be separate calls.
  5. I didn’t use an async call when making a blocking web request. This ties up my app pool threads more than necessary.

Now, a parting picture:

finalexample

 

And thats it, we now have a button on a CRM form that talks to CRM, start to finish everything that you need to know. 


Posted by David Eison on Wednesday, August 25, 2010 2:51 AM
Permalink | Comments (0) | Post RSSRSS comment feed