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

Disclaimer

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

© Copyright 2009

jQuery Validation. My first use and modification

I’m relatively new to jQuery.  But I needed a client-side validation framework.  Enter jQuery.validate.  Great little framework and easy to implement.  But I needed one more thing.  When a field WAS valid, I wanted it to display a success message.  Below is the (ugly, hardcoded) additions I made.  It gives me a nice little green checkmark when the field’s all nice and valid.

 

successes: function() {

      return $(this.settings.errorElement + "." + this.settings.successClass, this.errorContext);

},

 

successesFor: function(element) {

      return this.successes().filter("[for='" + this.idOrName(element) + "']");

},

 

showSuccess: function(me) {

      for (var i = 0; this.successList[i]; i++) {

            var success = this.successList[i];

            if (success) {

                  var message = "<img src=\"../../Content/Images/greencheckmark.png\" class=\"checkmark\" />"

                  if (this.settings.successClass) { } else { this.settings.successClass = "field-validation-success"; }

                  var label = this.successesFor(success);

                  if (label.length) {

                        // refresh error/success class

                        label.removeClass().addClass(this.settings.successClass);

 

                        // check if we have a generated label, replace the message then

                        label.attr("generated") && label.html(message);

                  } else {

                        // create label

                        label = $("<" + this.settings.errorElement + "/>")

                        .attr({ "for": this.idOrName(success), generated: true })

                        .addClass(this.settings.successClass)

                        .html(message || "");

                        if (this.settings.wrapper) {

                              // make sure the element is visible, even in IE

                              // actually showing the wrapped element is handled elsewhere

                              label = label.hide().show().wrap("<" + this.settings.wrapper + "/>").parent();

                        }

                        if (!this.labelContainer.append(label).length)

                              this.settings.errorPlacement

                  ? this.settings.errorPlacement(label, $(success))

                  : label.insertAfter(success);

                  }

 

                  if (this.settings.success) {

                        label.text("");

                        typeof this.settings.success == "string"

            ? label.addClass(this.settings.success)

            : this.settings.success(label);

                  }

            }

      }

      if (this.successList.length) {

            this.toShow = this.toShow.add(this.containers);

      }

      if (this.settings.success) {

            for (var i = 0; this.successList[i]; i++) {

                  this.showLabel(this.successList[i]);

            }

      }

      if (this.settings.unhighlight) {

            for (var i = 0, elements = this.invalidElements(); elements[i]; i++) {

                  var label = this.successesFor(elements[i]);

                  if (label.length) {

                        label.attr("generated") && label.html("");

                  }

            }

      }

      this.toHide = this.toHide.not(this.toShow);

      this.hideErrors();

      this.addWrapper(this.toShow).show();

},

 

 

 

And modified the following function:

// http://docs.jquery.com/Plugins/Validation/Validator/element

                  element: function(element) {

                        element = this.clean(element);

                        this.lastElement = element;

                        this.prepareElement(element);

                        this.currentElements = $(element);

                        var result = this.check(element);

                        if (result) {

                              delete this.invalid[element.name];

                        } else {

                              this.invalid[element.name] = true;

                        }

                        if (!this.numberOfInvalids()) {

                              // Hide error containers on last error

                              this.toHide = this.toHide.add(this.containers);

                        }

                        this.showErrors();

                   this.showSuccess();

                        return result;

                  },

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Trenton Adams on Tuesday, June 16, 2009 2:16 PM
Permalink | Comments (4) | Post RSSRSS comment feed

Linq To SQL and return types for dynamic SQL inside sprocs

LINQ TO SQL figures out what a method returns by executing it with SET FMTONLY ON, which causes SQL Server to not really run the method but instead just examine the tables and columns used.

Unfortunately, this completely doesn’t work for dynamic SQL, causing the LINQ designer to not be able to figure out the return type.  It even goes so far as to grays out the option for you to set the return type, forcing it to (none).

You can manually hack on the .cs file, but that file gets regenerated so it should be avoided.  Instead, if you just have a ‘get’ style sproc that doesn’t have bad side-effects, you can tell SQL Server that it’s ok to really run your sproc.

First, verify you have no bad side effects from running your sproc (e.g. that it’s ok to call it whenever Visual Studio thinks it wants to). 

Then, inside of the stored procedure, add:

SET FMTONLY OFF ;

Next, make sure your method runs ok with all null parameters. I do this by providing some reasonable values:

IF (@Param1 IS NULL)
BEGIN
  SET @Param1=0;
END

Finally, run it from Visual Studio to make sure it works:

SET FMTONLY ON;
Exec MySproc null, null, null
SET FMTONLY OFF;

If you get back columns, you’re great.  If not, check that your reasonable values work ok.

Finally, the LINQ designer does some aggressive caching of method return types. To change a return type, I have had to delete the method, save my project, close the connection in server explorer, exit visual studio, re-open the connection, and re-drag the method over for it to get over the (none) return type and let me pick one.  “Refresh” didn’t work.

Note that return type will stay as (none) in Visual Studio if it encounters a problem running your method, so be sure it works with SET FMTONLY ON; Exec [methodname] [null parameters] before trying to fight the cache problem.

DamienG's blog was the best source of info I found while troubleshooting this problem.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by David Eison on Thursday, June 04, 2009 12:16 AM
Permalink | Comments (3) | Post RSSRSS comment feed

Google Maps link in CRM

So I've been looking at a good way to get maps of locations direct from Dynamics CRM.  There are lots of neat solutions out there that embed a Google or Live map directly into a tab, but I didn't want the extra tab.  I wanted something simpler.  So I made this.  It's the simplest solution I could think of that effectively solve the issue.  It literally took more time to think of than to implement.

First,  go to customize the Entity that you want to have the link on.  I chose Account and Address for my implementation, but any entity with an address will do.  So first create an Attribute called Google Maps and put it on the form.  Make sure you give the Attribute a format of URL and increase the maximum length to 500 characters.  This will make it clickable form the UI and ensure you don't cut off the end of the address.

Now go to the Form and go to Form Properties. Open the OnLoad event and paste this JavaScript in:

crmForm.all.new_googlemaps.DataValue = "http://maps.google.com/maps?q=" + crmForm.all.line1.DataValue + "+" + crmForm.all.city.DataValue + "+" + crmForm.all.stateorprovince.DataValue + "+" + crmForm.all.postalcode.DataValue;

A few important things here:   "http://maps.google.com/maps?q=" is the beginning of the Google Maps query string. You need it.  The attributes after that, such as line1, city, etc. are specific to the Address entity.  If you want to do this in a different Entity, you'll have to find out what the specific name of the address, city, state and zip code fields are. also, these instructions are US-oriented.  For international addresses, you'll have to add whatever fields are relevant to get Google Maps to give you correct addresses.

In the end, you'll have a clickable field that will open up a map to the address in CRM in a new browser window. Very convenient, and also compatible with mobile CRM solutions!

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Categories: CRM
Posted by Wayne Walton on Friday, May 29, 2009 2:30 PM
Permalink | Comments (4) | Post RSSRSS comment feed

FedEx Integration into CRM 4.0

The FedEx Shipping manager software has a suite of integration features.  This allows one to import and export fields from external data sources including files, and ODBC connections.  After creating an ODBC connection to the CRM Database, imports were simple to setup.  But, the export was not working.  All the fields registered as read-only not matter how I set up the permissions.  I set about trying a number of work arounds.  First I started with trying to create an Access database to link to the CRM tables, but to no avail.  The FedEx integration assistant would not see the linked tables at all.  After trying a few more things with ODBC, I finally created a new database.  Within this database, I set up a single table with two columns (I was only wanting to export the tracking number for now).  An ID column and the tracking number.

 

OrderNumber TrackingNumber
   
   
   
   

 

I set up the integration in FedEx to insert a new row into this table everytime a shipment completed.  Then, to update the CRM SalesOrder.  I set an ‘on insert’ trigger on this table.  The trigger updated the appropriate record in CRM.

It’s a hack, but it worked.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Trenton Adams on Monday, May 18, 2009 10:11 AM
Permalink | Comments (2) | Post RSSRSS comment feed

Microsoft Dynamics CRM 4.0 Update Rollup 4 released

Why is seems like just yestarday we were posting about Update Rollup 3, and here comes Rollup 4!

 

You can find the KB article here: http://support.microsoft.com/kb/968176 

The actual files are here: http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=0ddf8e83-5d9c-4fe7-9ae6-f2713a024071#filelist  

Don't forget the updated help files!

One quick addendum, make sure you clear your Internet Explorer cache after installing on both the server and the client side.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Wayne Walton on Monday, May 11, 2009 2:16 PM
Permalink | Comments (5) | Post RSSRSS comment feed

Out of order CRM Annotations (Notes)

One of my import tools was having a problem where annotations were showing up sorted … oddly.  It would show for example annotations 4,5,6, and 7 followed by annotations 1, 2, and 3. 

Turning on trace shows that annotations were being selected from the database with a simple orderby createdon desc.  Sounds reasonable, sort by date, date should be uniquely ordered…

Repeat after me: CRM dates have no milliseconds. CRM dates have no milliseconds. CRMDateTime is a day + hh:mm:ss.  No milliseconds.

So, the createdon date was being truncated to the second, so any annotations that were created by my import tool during the same second all had the same value.  That leaves sql server with no particular directions for sorting them, so it picks the order it found them in, which happens to be the order they were created… so any annotation created within the same second sorts in creation order, then those are sorted in reverse creation order with other groups of annotations from other seconds. 

The solution is to put a second between annotation creations when importing if select order is going to be important.  If you don’t want to actually slow down your importing by a second every time you have a string of annotations, you can use the overriddencreatedon property to feed in adjusted-by-seconds times, but you have to count backwards because you can’t set a createdon date in the future.

Easy way:

System.Threading.Thread.Sleep(1000);

Hard way: (make sure the clock of whatever system you are running on is synced properly)

/// <summary>
/// Creates a CRM annotation 
/// </summary>
/// <param name="subject">Up to 500 chars, will be trimmed if necessary.</param>
/// <param name="text">Up to 5000 chars, will be trimmed if necessary.</param>
/// <param name="owningentitytypename">EntityName.[entity].ToString()</param>
/// <param name="owningentityguid">GUID.</param>
/// <param name="createtimeoffsetseconds">Usually this should be 0.  Must be 0 or negative, can't create things in the future.</param>
/// <param name="previousannotations">If previousannotations is specified, new annotation will be added with a 
/// create time 1 second before the earliest createtime in "previousannotations" list.  
/// When displayed in normal reverse chronological order, they will display in the 
/// actual order added by this method - which means you will usually want to add them 
/// in reverse chronological order also.</param>
/// <returns></returns>
public static annotation addAnnotation(string subject, string text, string owningentitytypename, Guid owningentityguid, int createtimeoffsetseconds, LinkedList<annotation> previousannotations)
{
    annotation annote = new annotation();
    // todo: determine how to not need to hardcode 500/5000.
    annote.subject = GetTrimmedIfNeeded(subject, 500);
    annote.notetext = GetTrimmedIfNeeded(text, 5000);
    annote.objectid = new Lookup();
    annote.objectid.type = owningentitytypename;
    annote.objectid.Value = owningentityguid;
    annote.objecttypecode = new EntityNameReference();
    annote.objecttypecode.Value = owningentitytypename;
    annote.isdocument = new CrmBoolean();
    annote.isdocument.Value = false;
    DateTime when = DateTime.Now.AddSeconds(1 + createtimeoffsetseconds);
    if (previousannotations != null && previousannotations.Count > 0)
    {
        foreach (annotation a in previousannotations)
        {
            CrmDateTime comp = a.createdon ?? a.overriddencreatedon;
            if (comp != null)
            {
                if (comp.UserTime.CompareTo(when) < 0)
                {
                    when = comp.UserTime;
                }
            }
        }
    }
    annote.overriddencreatedon = CrmHelper.ConvertToCrmDateTime(when.AddSeconds(-1 + createtimeoffsetseconds));
    GetCrmService().Create(annote);
    return annote;
}

public static annotation addAnnotation(string subject, string text, string owningentitytypename, Guid owningentityguid)
{
    return addAnnotation(subject, text, owningentitytypename, owningentityguid, 0, null);
}

/// <summary>
/// Overly fancy trimming function that trims text down to fit in length.
/// Tries to include a note indicating that trimming was done.  
/// Note is longer if more space is available.
/// Examples:
/// ?CrmHelper.GetTrimmedIfNeeded("01234567890123456789012345678901234567890123456789", 5)
/// "01234"
/// ?CrmHelper.GetTrimmedIfNeeded("01234567890123456789012345678901234567890123456789", 6)
/// "01234*"
/// ?CrmHelper.GetTrimmedIfNeeded("01234567890123456789012345678901234567890123456789", 11)
/// "01234567..."
/// ?CrmHelper.GetTrimmedIfNeeded("0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", 51)
/// "012345678901234567890123456789012345678901(trimmed)"
/// ?CrmHelper.GetTrimmedIfNeeded("012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", 151)
/// "012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678(trimmed to 151 chars)"
/// </summary>
/// <param name="str">String to maybe trim</param>
/// <param name="length">Max length after trimming, including trim note (if any)</param>
/// <returns>String no longer than length, maybe trimmed, maybe with a note indicating the trimming was done.</returns>
public static string GetTrimmedIfNeeded(string str, int length)
{
    if (str.Length > length)
    {
        string trimmedwarning="";
        if (length > 150) {
            trimmedwarning="(trimmed to " + length + " chars)";
        } else if (length > 50) {
            trimmedwarning="(trimmed)";
        } else if (length > 10) {
            trimmedwarning="...";
        } else if (length > 5) {
            trimmedwarning="*";
        }
        int warninglength = trimmedwarning.Length;


        return str.Substring(0, (length-warninglength)) + trimmedwarning;
    }
    else
    {
        return str;
    }
}

Currently rated 1.0 by 1 people

  • Currently 1/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by David Eison on Friday, April 10, 2009 3:23 AM
Permalink | Comments (7) | Post RSSRSS comment feed

Microsoft Dynamics CRM 4.0 Update Rollup 3 released

While at Convergence last week, Microsoft went ahead and released Update Rollup 3 for CRM 4.0.  You can get it here.

A few important notes:

  • Importing and exporting customizations is supported between servers with Update Rollup 2 and 3, but not supported between Release, Rollup 1 and Rollup 3. 
  • The CRM for Outlook Client has some memory usage issues resolved.
  • Performance issues with CRM related to the email router have been resolved.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Categories: CRM
Posted by Wayne Walton on Tuesday, March 17, 2009 10:26 AM
Permalink | Comments (10) | Post RSSRSS comment feed

Truncate_Only no longer supported in SQL Server 2008

If you've ever used the Truncate_Only to shrink logs in SQL Server 2000/2005, it may come as a surprise to you that its use has been discontinued in SQL Server 2008.

Instead, you can use the following commands to get SQL Server to do essentially the same thing:

 
Alter Database %databasename% Set Recovery Simple

 And then

Alter Database %databasename% Set Recovery Full

You can then shrink the log file as normal.

Currently rated 4.0 by 1 people

  • Currently 4/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Categories: SQL Server
Posted by Wayne Walton on Monday, March 16, 2009 3:04 PM
Permalink | Comments (53) | Post RSSRSS comment feed

Unable to load client print control – SSRS and CRM

After some Googling, I came across this forum post: http://social.microsoft.com/Forums/en-US/crm/thread/b86740b6-6418-4e1c-9020-1d6c9c630b7b/

Basically a hotfix KB956391 broke the client control for SSRS.  We had already installed all the latest automatic updates on all the servers, but the following update isn’t presented with Windows Update.

http://www.microsoft.com/downloads/details.aspx?FamilyID=82833f27-081d-4b72-83ef-2836360a904d&DisplayLang=en

Installing this fix on ALL* related servers, then rebooting fixed the problem.  (*You can’t just install it on the SSRS machine, you need to install it on any server that allows a client to connect to reporting services.) 

Basically, this fix forces the browser to download the latest client viewer and install it.  I had found a number of other solutions that required a manual installation on the client’s computer, but this one works from the server side.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Categories: CRM | SQL Server | SSRS
Posted by Trenton Adams on Wednesday, March 11, 2009 5:53 PM
Permalink | Comments (29) | Post RSSRSS comment feed

Importing related records in CRM from CSV

So it is a common issue that when new CRM clients (especially CRM Online clients) come online, they have data they need to import.  Well, not all of them have proper databases, or even really a budget to get a custom-developed import solution, or Scribe.  Well, we've put together a budget-oriented method that can help.

First off, the most common issue is that the client has their info in just one big Excel file.  Contacts, Company, Notes and maybe even Opportunities are all in one big flat file.  Well obviously that won't do for CRM.  However, if you just break them up into different files and upload, they aren't associated with one another.  Now if you only have a few dozen contacts and companies, it might be ok to go in after the fact and set them manually, but that's not going to be a viable solution for most.  So we need a way to associate all these different entities.

Fortunately, CRM makes a unique identifier for every record it creates (called a GUID), and we can use this record to tie record types together.  However, to do this, you're going to need a couple tools.

1. The Microsoft CRM Data Migration Manager.  This comes in both a local/hosted version and a CRM Online version. Don't get these confused, they are not compatible.  Also, I very strongly recommend setting up a virtual machine to install these on, it will save you a lot of heartache in the future, and you can avoid some "gotchas" about how the DMM works on a clean VM.

2. The CRM Bulk Data Export Tool.  This is Arke Systems' updated version of the Bulk Data Export Tool. The changes we made include being able to get GUIDs out of CRM, and not having to set a date limit for how much data you're getting out of CRM.  If you have any trouble with the app, please post the errors, and we can take a look. It's not technically a supported app by us or by MS, but I'll try to help.

So now that we have the tools we need, let's get down to brass tacks.  First things first, extract the information you need about the Account into its own CSV file.  At the minimum, you'll need a Company Name.  Once you have that file, go ahead and upload it to CRM via the Data Migration Manager.  Once that's done, open up the Bulk Data Export Tool. Authenticate to your CRM, and then export the Accounts back out.  Make sure to set in the dropdown that you want the IDs exported too.  That this does is give us the GUID of every Account you just uploaded to CRM.

Below: The Bulk Export Tool.  Important field highlighted in red.

Now that we have those GUIDS, we need to find a way to set those in your Contacts list (or whatever list you need to associate with those Accounts).  Sure, you could just copy and paste those GUIDs next to the appropriate contact, but that would take forever.  What we need is a little Excel magic.  First, you're going to need to understand how to use the OFFSET and MATCH functions in Excel. Say you have an Excel document with two sheets in it (this can also work across documents).  the first one has a list of all companies and the related GUIDs.  The second one has a list of contact names and the companies they are part of.  So on the Contacts sheet, you're going to add two more columns, Match and Account ID.  On the Match column, you're going to write something like this: =MATCH(A2,Sheet1!A:A,0).  What that tells me is that on Sheet1 (where the Company list resides), I want to find the relative position of the Company Name and report a row number back.  On the Account ID column, you'll put something that looks like this: =OFFSET(Sheet1!$B$2,Sheet2!B2-2,0). What that means is that you want Excel to go look on Sheet1 and grab the GUID relative to the Match number you've pulled to Sheet2 (the Contacts sheet).

Below: Sheet2 and Sheet1 examples

So now you should have a list of contacts that also has a list of GUIDs under the heading Account ID.  Save that file as a raw CSV, it's time to go back to the Data Migration Manager.  Start another migration with your contacts, and match up fields like normal until we get to the Account ID. For this one, we're going to do things a little different.  First, select the field in CRM called "Parent Customer".  When you do, it should take you to another page asking you to link up this field with one in Accounts.  That's good, we want that. So select the dropdown that says Account ID and continue on.

Assuming the rest of the migration goes smoothly, you should be able to go into a Contact and see that it has a Parent Customer all set. If you prefer to have the Primary Contact field in Accounts populated instead, just reverse the order of uploads.  Do Contacts first, and then upload Accounts with the Primary Contact ID instead. Speaking of which, that is a limitation of this method.  You will always have one standalone Entity at the beginning (normally Accounts or Contacts).  I have not found a good way around that, yet.

So I hope that helps all of you that have been struggling with a simple, yet reasonably effective way of importing data to CRM on a budget.  Please hit up the comments with suggestions on improving this, or any questions you have about the process!

Currently rated 5.0 by 2 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Categories: CRM
Posted by Wayne Walton on Monday, March 02, 2009 12:09 PM
Permalink | Comments (15) | Post RSSRSS comment feed