Programming, technology, and CRM – from a Belgian programmer exiled to Missouri
  • rss
  • Home
  • Contact Me
  • Welcome

Picklist Bundle Assistant – Now with Integrated Bundler

Nicolas Galler | December 29, 2008

Laziness being what it is I got tired of having to open and save each project separately after my bundle assistant created them and added an option to generate the bundle files directly from the tool.

It’s not perfect but it does what I need (except for a better interface to capture the picklist values – maybe next time).

image

Comments
No Comments »
Categories
Uncategorized
Comments rss Comments rss
Trackback Trackback

Picklist Bundle Assistant

Nicolas Galler | December 26, 2008

Today I finally broke down.  Confronted with the task of creating 15 picklist bundles I decided to (slightly) automate the task with a tool.  It is very simple (does what I need and nothing more) – basically given a list of picklist names (the picklists must already exist) it will create the corresponding projects.

image

I think in the near future I will prepare an improvement to also make it easier to enter the picklist values (right now I use the import feature from the Architect but it still require you to create and set up the picklists one by one) – but that’s for another day.

The code is available on MSDN Code Gallery.

Comments
No Comments »
Categories
Uncategorized
Comments rss Comments rss
Trackback Trackback

Authenticate Users with Active Directory (in ASP.NET 1.1)

Nicolas Galler | December 19, 2008

If you need to authenticate user logins against their Active Directory credentials, integrated authentication is often preferable since it is automatic but it is not always possible.  Given clear text user name and password, how can we do it?

A quick search will turn up a lot of ways:

  • Use the LogonUser API (possibly not recommended as this is really used to impersonate a user, not just validate their login, but it appears if you use LOGON32_LOGON_NETWORK as logon type then it is a valid way to do it)
  • Use the SSPI API – I’ll admit I was not able to go through that document
  • Use an LDAP connection object (with a special OleDb provider)
  • Use an LDAP object (with the GetObject call in VBScript – not sure what the equivalent is in C# though)
  • Use the System.DirectoryServices

Of the above the DirectoryServices is the only one that worked for me on ASP.NET 1.1. 

The authentication procedure looked something like this:

private bool ValidateLogin(String user, String pw)
{
  DirectoryEntry e = new DirectoryEntry("GC://DOMAIN", user, pw, AuthenticationTypes.Secure);
  try {
    if(e.Name != null)
      return true;
  } catch {
    // exception means the logon failed
    return false;
  }
  // should not be reached
  return false;
}

The “GC://DOMAIN” part was the little trick… first I tried “WinNT://DOMAIN/USER” and found out new users were not able to log in.  You can also use a full LDAP query string.

Comments
No Comments »
Categories
Uncategorized
Comments rss Comments rss
Trackback Trackback

First Impressions of ASP.NET MVC

Nicolas Galler | November 4, 2008

I finally got to take a look at ASP.NET MVC today for a small project (a basic lead capture kind of functionality).  It is a real breath of fresh air compared to web forms – all you have is very straightforward HTTP.  It also offers a very good separation between the UI and the application logic, and writing Javascript is a lot easier without having to worry about UpdatePanel’s and mangled control ids.  One of my favorite features is the data binding.  A few times I have banged my head against the ASP.NET web form data binding without finding a very satisfying answer.  ASP.NET MVC makes it amazingly easy – instead of having code like the following:

<asp:TextBox runat="server" id="txtLastName"/>
Contact c = new Contact();
c.LastName = txtLastName.Text;
// ...

You’ll have:

<input type="text" name="contact.LastName"/>

and a “controller” function taking a Contact object as parameter:

ActionResult SaveContact(Contact c){
// ...
}

There are a few variations of course but the basic case is very simple.  To use webform databinding in this case you may have to use a FormView, create the various templates, create and configure the DataSource, etc – what a headache.

Still, this is no Django.  The modularity of Django in particular is far ahead and an ASP.NET MVC application seems very “monolithic” in nature.  This is one area where webforms (with user controls and custom controls) are actually good.  The route configuration is not very elegant (compared to Django).  The documentation is poor, and the API still appears to be very unstable (I don’t mean that as buggy – just that it is mutating very fast).

I would probably use it again on a blank slate project, and I think it will get better with time.

Comments
No Comments »
Categories
Uncategorized
Comments rss Comments rss
Trackback Trackback

1st Saleslogix .NET Extension

Nicolas Galler | September 17, 2008

Technically not the very first one as I have played with it before but this is the first case where it actually allows me to do something that would have been impossible without it.

One very useful resource was this page on SlxDeveloper: http://www.slxdeveloper.com/page.aspx?id=56.  All articles are very interesting but this one in particular is full of example and extremely useful.  Thanks so much to Ryan for putting this together.

The extension in question is relatively simple, it builds a logo dynamically and displays a preview in the client.  As there is no way to manipulate graphics in VBScript without the .NET extensions this would have required an external program or a custom COM component and distributing that to the remotes could have been a real hassle.

There were a few things to note although the explanations on the afore-mentioned articles were so good that I won’t repeat them here:

  • The .NET extension has to be compiled against the exact same version of Saleslogix that is going to be used on the client.  This implies that it will have to be relinked after an upgrade.  Otherwise, you won’t get a clear error message, but the call to Application.Managed.Run will fail.
  • The Saleslogix assemblies (Sage.SalesLogix.NetExtensions.Framework.dll and Sage.SalesLogix.NetExtensions.Core.dll) have to be registered in the GAC.  If they aren’t, you have to add them (either use gacutil, or simply drag/drop them to C:\Windows\Assembly in the explorer).  Normally this is done by the installer but apparently this sometimes fail.
  • The .NET extension manager does not run on 64bit OS.  So you have to register the extensions on a separate machine.
  • The .NET extension bundles are not terribly reliable… I think you are better off just registering them by hand on the production machine.
  • This is the code that I use to load my custom control and have it build and display the logo:
    handle = Application.Managed.Create("Barrel Logo Extension", _
        "SSSWorld.BarrelLogoExtension.SlxLoader")

    On Error Resume Next
    Set logo = Application.Managed.Run(handle, panLogoContainer.HWND)
    If Err.Number <> 0 Then
        MsgBox "Error loading logo control: " & Err.Description & vbCrLf & _
            "Please verify that you have the current Saleslogix service pack (v7.0.1)." & vbCrLf & _
            "If this error persists, please contact your Administrator.", _
            vbCritical, "Error"
        ModalResult = mrCancel
        Exit Sub
    End If
    logo.LoadBarrelLogo g_sFileName, g_sLine1, g_sLine2, g_sLine3, g_sLine4, g_sLine5

The “BaseRunnable” implementation (called SlxLoader) returns a ComVisible object that can then be manipulated by the VBScript.  First I tried having it return the UserControl directly but this would not go through (presumably COM did not know how to control it).  However you do not have to register the COM interface using regasm or the “Register for COM interop” checkbox – just having the type bear the ComVisible attribute is enough.  And the object in question holds a reference to the UserControl – this is the implementation of the LoadBarrelLogo method:

public void LoadBarrelLogo(String imgPath, String line1, String line2, String line3, String line4, String line5)
{
  _logo.LoadBarrelLogo(imgPath, line1, line2, line3, line4, line5);

}

and the Run method:

public override object Run(object[] args)
{
    if (args.Length != 1 || !(args[0] is Int32))
        throw new InvalidOperationException("Usage: SlxLoader.Run(hwnd)");

    IntPtr formHwnd = new IntPtr((int)args[0]);
    _logo = new BarrelLogo();
    Win32Util.SetParent(_logo.Handle, formHwnd);
    SSSWorld.Common.Win32Util.RECT formSize;
    if (Win32Util.GetWindowRect(formHwnd, out formSize))
    {
        _logo.Size = formSize.Size;
    }
    return this;
}

This code is explained in detail in the SlxDeveloper article.

These .NET extensions are a very nice tool to have.  There is definitely some room for improvement in the implementation, though sadly as the focus has shifted away from the network client it is unlikely that we will see any.  In most cases if it is possible to do it in VBScript instead it will be easier to manage in the long term.  But having the possibility to do more if absolutely needed is good. 

Comments
4 Comments »
Categories
Saleslogix
Comments rss Comments rss
Trackback Trackback

A multi-select picklist for MS CRM

Nicolas Galler | August 22, 2008

One of the difference between the MSCRM and the Saleslogix editors is the richness of the controls included with Saleslogix.  For example, the picklist available by default with MSCRM does not support multiple selection.  Fortunately it is easy to remedy using a little bit of Javascript in the form.

The key requisites were:

  • should be able to manage the values for the picklist using the same interface as regular picklists
  • should not have to customize the code (just paste the base code and add one line to initialize the picklist)
  • does not disrupt tab navigation (I did not 100% succeed on that one because you can only tab forward, not backward)

image

To make it work I simply created 2 attributes: "HobbiesList" points to the picklist, and "Hobbies" saves the actual values (this wastes a field in the contact table but we only lose 4 bytes).  Both attributes are present on the form but at runtime the HobbiesList is hidden and converted to a multiple selection list to be displayed only when you tab into the Hobbies textbox.

image

This is the javascript I used in the "Load" event on the form.  This part is generic and does not have to be customized – I just paste it at the beginning of the code.  I struggled a bit to get the positioning right, the rest is pretty classic stuff:

// make the textbox act as a multi select picklist including the elements of the 
// given select control
function makeMultiSelectPicklist(txt, pkl){
    txt.associatedPicklist = pkl;
    pkl.associatedTextbox = txt;
    txt.readonly = true;

    // hide containing row
    for(var p = pkl; p ; p = p.parentNode){
    if(p.tagName == "TR"){
        // hide the entire row (the picklist will be moved out)
        p.style.display = "none";
        break;
    }
    }

    // setup position for the multi select picklist
    var container = document.createElement("span");
    container.style.position = "relative";
    container.style.top = "0px";
    container.style.left = "0px";
    container.style.width = "0px";
    container.style.height = "0px";
    txt.parentNode.insertBefore(container, txt);
    container.insertBefore(pkl, null);
    pkl.style.display = "none";
    pkl.style.position = "absolute";
    pkl.style.top = "0px";
    pkl.style.left = "0px";
    pkl.multiple = "true";

    txt.onfocus = function(e) {
    var txt = this;
    var pkl = txt.associatedPicklist;
    var values = "|" + txt.value.replace(/; /g, "|") + "|";
    pkl.style.display = "block";
    pkl.style.width = txt.offsetWidth + "px";
    for(var i=0; i < pkl.options.length; i++){
        pkl.options[i].selected = new RegExp("\\|" + pkl.options[i].text + "\\|").test(values);
    }
    pkl.focus();
    }
    pkl.onblur = function(e) {
    var pkl = this;
    var txt = pkl.associatedTextbox;

    var s = [];
    for(var i=0; i < pkl.options.length; i++){
        if(pkl.options[i].text && pkl.options[i].selected){
        s.push(pkl.options[i].text);
        }
    }
    txt.value = s.join("; ");
    pkl.style.display = "none";
    }

    // break reference to avoid leak on IE6
    txt = null;
    pkl = null;
}

And finally I add this line at the end of the Load event to initialize the picklist:

makeMultiSelectPicklist(document.all.new_hobbies, document.all.new_hobbieslist);

A little bit more work than in Saleslogix but not too terrible!  And, it works somewhat better for tab navigation.  One thing worth noting – the Javascript editor on the CRM designer is so terrible, it is a lot easier to do the development in a separate file then paste it in.

Comments
1 Comment »
Categories
MSCRM
Tags
Javascript, MSCRM
Comments rss Comments rss
Trackback Trackback

Automatic Assembly Version Number

Nicolas Galler | August 6, 2008

Under Properties\AssemblyInfo.cs you can set the version number – this shows up under the file’s details in Windows:

image

There is a nifty msbuild task that was distributed by Microsoft a while back called "AssemblyInfoTask" (I think it came with one of the Team Foundation Server releases) that can automatically set it to the current date which is very handy to quickly find out which version of a particular DLL is at a customer site.  However there are a few issues with the strategy:

  • because the version number is a 16-bit number it has some issues with assemblies built after 2007 (there is a work around for it – as you can see I only have month and day)
  • it updates AssemblyInfo.cs (no easy way to customize that) which is usually under version control… so this causes a lot of unnecessary commits and conflicts (trivial to resolve, but still annoying)
  • since it updates AssemblyInfo.cs every time it runs and not just when the version number needs to change it will cause the assembly to be rebuilt completely every time which makes the build process slower.

Alternatively there is a task in the "MSBuild Community Task" that can be used to generate a VersionInfo.cs file – just as good as AssemblyInfo but because this only contains the version it does not need to be checked into source control alleviating the 2nd issue above (at this point you comment out the AssemblyVersion attribute from the AssemblyInfo.cs file).

Another one from MSBuild Community Task is "SvnInfo" – it can be used to retrieve the Subversion revision number (there are other, equivalent tasks for other version control system).  Using this is just as good if not better than the date so we avoid the 1st issue (though this will come back when my SVN number gets above 65536!  But I have a long way to go for that).

There was still one issue which is the file would always be generated thus preventing the incremental build from shortening the build process as it is supposed to (in C or Java we can somewhat get away with that since the objects are built on a file-by-file basis but in C# they are always built into assemblies).

Therefore I created a very very simple task (don’t laugh, it is my first msbuild custom task!) that generates the file but replaces it only if the number is updated:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Build.Utilities;
using System.IO;

namespace SSSWorld.MsBuild
{
    /// <summary>
    /// Task used to generate a VersionInfo file with the AssemblyVersion and AssemblyFileVersion
    /// attributes.
    /// Contains logic to avoid overwriting the file if it exists and the content is identical.
    /// </summary>
    public class GenerateVersionInfo : Task
    {
        public UInt16 MajorNumber { get; set; }
        public UInt16 MinorNumber { get; set; }
        public UInt16 RevisionNumber { get; set; }
        public UInt16 BuildNumber { get; set; }
        public String OutputFileName { get; set; }

        public GenerateVersionInfo()
        {
            MajorNumber = 0;
            MinorNumber = 0;
            RevisionNumber = 0;
            BuildNumber = 0;
            OutputFileName = null;
        }

        public override bool Execute()
        {
            String code = GenerateCode();
            String existingCode = null;
            if (File.Exists(OutputFileName))
                existingCode = File.ReadAllText(OutputFileName);
            if (!code.Equals(existingCode))
            {
                File.WriteAllText(OutputFileName, code);
                Log.LogMessage("Generated VersionInfo file {0}", OutputFileName);
            }
            else
            {
                Log.LogMessage("VersionInfo file {0} is up to date", OutputFileName);
            }

            return true;
        }

        private String GenerateCode()
        {
            StringBuilder buf = new StringBuilder();

            buf.AppendLine("// Code generated automatically by SSSWorld.MsBuild.GenerateVersionInfo")
                .Append("[assembly: System.Reflection.AssemblyVersion(\"")
                .Append(MajorNumber).Append(".")
                .Append(MinorNumber).Append(".")
                .Append(RevisionNumber).Append(".")
                .Append(BuildNumber == 0 ? "*" : BuildNumber.ToString())
                .AppendLine("\")]")
                .Append("[assembly: System.Reflection.AssemblyFileVersion(\"")
                .Append(MajorNumber).Append(".")
                .Append(MinorNumber).Append(".")
                .Append(RevisionNumber).Append(".")
                .Append(BuildNumber)
                .AppendLine("\")]");
            return buf.ToString();
        }
    }
}

And I adapted the AssemblyInfoTask target file to call on this task:

<?xml version="1.0" encoding="utf-8"?>
<!-- This targets file includes all the necessary information to automatically increment build numbers as part of
     a regular build process. To use it simply include it in your project file after any other includes. The typical
     include line looks like this:
     
     <Import Project="...\SSSWorld\msbuild\AssemblyVersion.target"/>
     
     and make sure you have a Properties\VersionInfo.cs file in your project (it will be overwritten by this task
     and should NOT be checked in version control)
  -->
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <UsingTask AssemblyFile="SSSWorld.MsBuild.dll" TaskName="SSSWorld.MsBuild.GenerateVersionInfo"/>

  <!-- These properties can be overridden to specify the major/minor version number -->
  <PropertyGroup>
    <MajorNumber Condition="'$(MajorNumber)'==''">1</MajorNumber>
    <MinorNumber Condition="'$(MinorNumber)'==''">0</MinorNumber>
    <!-- Default will be to get the SVN revision number as revision number -->
    <RevisionNumber  Condition="'$(RevisionNumber)'==''">0</RevisionNumber>
  </PropertyGroup>

  <!-- Re-define CoreCompileDependsOn to ensure the assemblyinfo files are updated before compilation. -->
  <PropertyGroup>
    <CoreCompileDependsOn>
      $(CoreCompileDependsOn);
      GenerateVersionInfoFile
    </CoreCompileDependsOn>
  </PropertyGroup>

  <Import Project="$(MSBuildExtensionsPath)\MSBuildCommunityTasks\MSBuild.Community.Tasks.Targets"/>    

  <Target Name="GenerateVersionInfoFile">
    <SvnInfo LocalPath="." Condition="$(RevisionNumber) == 0">
      <Output TaskParameter="Revision" PropertyName="RevisionNumber"/>
    </SvnInfo>
    <GenerateVersionInfo OutputFileName="Properties\VersionInfo.cs"
      MajorNumber="$(MajorNumber)"
      MinorNumber="$(MinorNumber)"
      RevisionNumber="$(RevisionNumber)"/>
  </Target>
</Project>

What then needs to be done to get the version number into any project is to:

  • Remove AssemblyVersion and AssemblyFileVersion attribute from AssemblyInfo.cs
  • Add a VersionInfo.cs under the Properties folder of the project (does not have to contain anything as it will be overwritten automatically – and make sure it is NOT checked into source control)
  • Manually edit the project file and add the import line… Make sure the DLL is in the same directoy as the target file.
  • If desired, the Major and Minor version numbers can be specified in the project file by adding a “MajorNumber” or “MinorNumber” property under the <PropertyGroup> element (typically near the very top of the project file)

Optionally the following command can also be run to prevent the "unsafe project" warning dialog (replace with actual path of target file):

reg add HKLM\Software\Microsoft\VisualStudio\9.0\MSBuild\SafeImports /v AssemblyVersion /t REG_SZ /d "E:\Projects\SSSWorld\msbuild\AssemblyVersion.target"

Note that the key name is HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\9.0\MSBuild\SafeImports on Windows64.

Instead of using SvnInfo to get the revision number it can be obtained using the <Time> task, also part of the MSBuild Community Task.  Just make sure that the number is below 65536 as it needs to fit in 16 bits.

It is very simple but I have uploaded it to http://code.msdn.microsoft.com/AssemblyVersion in case it is useful to anybody else.  It was interesting at least to see how easy it was to create and use an msbuild custom task – I was relieved to find that it was not necessary to sign the assembly and install it to the GAC, and that you can use it right away.

Comments
2 Comments »
Categories
Programming
Comments rss Comments rss
Trackback Trackback

Web Mailmerge on Vista

Nicolas Galler | July 2, 2008

Compared to the instructions at http://customerfx.com/pages/crmdeveloper/2008/02/21/solving-thttprequestthread-errors-with-activemail-in-saleslogix-web.aspx there are a few different steps to get the SLX Mailmerge piece to work on a Vista web server (IIS7).

  • Configure the web manager, same as on 2003 or XP (same as the instructions on Ryan’s post)
  • Create the /mmserver virtual directory and point it to C:\PF\SLX\Web Components\mmserver (same as before).  Make sure you create it as an Application and select the correct type of Application Pool (classic pipeline, 32bit enabled if running on Vista64)

image

  • Under mmserver click "Handler Mappings" and enable "ISAPI-dll":

image

image

  • On the server configuration (click on the server name), select "ISAPI and CGI Restrictions" and add an entry for the slxwebmm.dll:

image

image

Update (2008-08-23): Same instructions work with Windows Server 2008. We had to make sure that the mmserver and the SlxClient applications were both running in the same application pool for some reason.

Comments
1 Comment »
Categories
Saleslogix
Tags
Saleslogix, SlxWeb, slxwebmm
Comments rss Comments rss
Trackback Trackback

Hosting WCF Services within SalesLogix

Nicolas Galler | June 24, 2008

The recent releases of WCF brought a whole bunch of goodies: most notable for me are the added support for syndication and script services, as well as a "no-configuration" scenario where we did not need to add a whole page of XML in web.config anymore just to get a basic service running.

I knew that there would be a few problems to getting it to collaborate with the Saleslogix web client app because of its tight integration with the ASP.NET pipeline, but wanted to take a look anyway.

I started with this very simple service file:

<%@ ServiceHost Language="C#" Debug="true"
    Service="SSSWorld.Scratch.SimpleService"
    CodeBehind="SimpleService.svc.cs"
    Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory,
      System.ServiceModel.Web,
      Version=3.5.0.0,
      PublicKeyToken=31bf3856ad364e35" %>

The key points is the "WebScriptServiceHostFactory.  Basically this automatically configures the endpoint for WCF without the need to add it to the web.config.

My SimpleService.cs at this point is also very simple:

public class SimpleService : ISimpleService
{
    public String GetData()
    {
        return "hello";
    }
}

And this is the corresponding "ServiceContract" (aka interface):

[ServiceContract]
public interface ISimpleService
{
    [OperationContract]
    [WebGet]
    String GetData();
}

The first problem I ran into appears to have actually been an installation problem.  I kept getting an error "Unable to load System.ServiceModel.Web".  Eventually I re-registered the assembly with the GAC (with gacutil /i /F) and all was good on that side.

Next was an IIS configuration problem.  Apparently integrated Windows authentication needs to be disabled.  So I cut it off (if you still wanted it to work with Saleslogix you could just cut it off for the folder containing the .svc file, I suppose) and finally got a

{"d":"hello"}

back at me.

Next I wanted to be able to access the Saleslogix service, so I added the following code:

if (ApplicationContext.Current == null)
                throw new InvalidOperationException("No Application Context");

Got the expected exception thrown.  I knew that ApplicationContext relies on classic ASP.NET sessions and also has some hard-coded dependencies to HttpContext.Current so I needed to enable the ASP.NET compatibility of WCF.  This is done with 2 changes, first I needed to add an attribute on the SimpleService class:

[AspNetCompatibilityRequirements(RequirementsMode=AspNetCompatibilityRequirementsMode.Required)]

And I also needed the following blurb in web.config (which I was a little bit sad about since I was hoping not to have to modify it, but oh well):

<system.serviceModel>
  <serviceHostingEnvironment aspNetCompatibilityEnabled="true"/>
</system.serviceModel> 

At this point I got my "hello" back.

Now it was time to see how far we could go!  I changed "GetData" into a simple search service – what it would do is simply return a little bit of information about accounts matching the input string (well, just the account name, for now):

public String GetData(String search)
{
    if (ApplicationContext.Current == null)
        throw new InvalidOperationException("No Application Context");
    if (String.IsNullOrEmpty(search))
        return "";
    using (SessionScopeWrapper session = new SessionScopeWrapper(true))
    {
        var matches = session.CreateQuery("from Sage.SalesLogix.Entities.Account a where a.AccountName like ?")
            .SetString(0, search + "%")
            .List<IAccount>();
        if (matches.Count > 0)
        {
            return matches[0].AccountName;
        }
        else
        {
            return "";
        }
    }
}

Go to http://…../SimpleService.svc/GetData?search=Abbott (going through the login page if necessary) and this returns:

{"d":"Abbott Ltd."}

as expected.

Getting pretty close!  Now if you wanted to return a list with some more info about each account, how about this:

  • Create a data contract:
[DataContract]
public class AccountInfo
{
    [DataMember]
    public String AccountName { get; set; }

    [DataMember]
    public String MainPhone { get; set; }

    [DataMember]
    public String City { get; set; }
}
  • Change the declaration:
[OperationContract]
[WebGet]
IList<AccountInfo> GetData(String search);
  • Change the "return matches[0].AccountName" line into something like this:
var q = from m in matches
        select new AccountInfo
        {
            AccountName = m.AccountName,
            MainPhone = m.MainPhone,
            City = m.Address.City
        };
return q.ToList();

Well, you get the idea.  The data can be recovered from a script service on an ASP.NET page, etc.

Now there is one serious limitation – this requires the user to be properly authenticated (because otherwise SalesLogix can’t form the connection string!).  If we need the service to be accessible from an outside client (eg for a mashup or an RSS feed) you would have to either somehow simulate a user login, or forego the whole ApplicationContext, host your service outside of the web client, and maintain the connection string for it separately.  At this point you could either use the technique I outlined in my "Unit Testing" posts to still get access to the NHibernate session (and to the entity business rules!), or just use regular ADO.NET to retrieve the data (probably easier, more reliable, and yielding better performance, unless you need access to the business rules.

Anyway, this was an interesting exploration, I have not decided if I would make anything of it yet.

Comments
1 Comment »
Categories
Uncategorized
Comments rss Comments rss
Trackback Trackback

Sending an email from Saleslogix (or ASP.NET)

Nicolas Galler | June 23, 2008

As the .NET framework is available to code executing on the server side of the Saleslogix web client it is easy to send an email using System.Net.Mail etc.  Lots of available examples for that so I won’t repeat them.

The problem with this approach (for some scenario, anyway) is that the user does not have a chance to review and edit the text the email (or attach it to Saleslogix for that matter).  Sometimes it would be nicer to just open the email in Outlook and let them complete it, and in some cases it is possible with a little bit of JavaScript, as long as you are able to get your users to adjust their IE security settings to enable unsafe ActiveX (which they have to do for the export to Excel anyway).

My problem was a simple "Quote" screen where the user should be able to print the quote report, have it attached to history and have the opportunity to send an email to the quote contact:

image

This is simple enough to do in the regular Saleslogix client – in the web client there are 2 problems: one is creating and displaying the message in Outlook, the other one is attaching the report (which was exported as a PDF on the server but will not be available from the client side).

In order to resolve the second problem, and since we can give Outlook a URL to attach, I created a "GetReport" service that is placed in an unsecured directory (since it will have to be accessed from Outlook) and simply returns the PDF data.  As a side note, to unsecure a page, place a web.config file in its directory with the following:

<?xml version="1.0"?>

<configuration>
  <location path="GetReport.ashx">
    <system.web>
      <authorization>
        <allow users="*"/>
      </authorization>
    </system.web>
  </location>
</configuration>

Now you can get to it without logging in.  Which is OK for this service since you need to have a report token in order to retrieve anything.

To resolve the first problem I sent some code (with ScriptManager.RegisterClientBlock) that will open Outlook on the client, fill in the subject and recipient, and display the message so the user can finish editing it:

public void ShowReportEmail(Uri reportUrl, string to, string cc, string subject, string body)
{
    StringBuilder script = new StringBuilder();

    var msgData = new { to = to, cc = cc, subject = subject, body = body, reportUrl = reportUrl.ToString() };
    var serializer = new System.Web.Script.Serialization.JavaScriptSerializer();
    script.Append("try {\n" +
        "var outlook = new ActiveXObject(\"Outlook.Application\");\n" +
        "var msg = outlook.CreateItem(0);\n")
        .AppendFormat("var msgData = {0};\n", serializer.Serialize(msgData))
        .Append("msg.Subject = msgData.subject;\n" +
            "msg.Body = msgData.body;\n" +
            "msg.To = msgData.to;\n" +
            "msg.Cc = msgData.cc;\n" +
            "msg.Attachments.Add(msgData.reportUrl);\n" +
            "msg.Display();\n" +
            "} catch(e) {\n" +
            "  alert('Error accessing outlook: ' + e.description + '.\\nPlease consult your administrator on recommended IE settings.');\n" +
            "}");

    ScriptManager.RegisterClientScriptBlock(this, GetType(), "EmailReport", script.ToString(), true);
}

The code makes use of the anonymous object syntax so requires VS 2008 to compile (should not require .NET 3.5 to run, though).  You could adapt easily if needed.

This probably works on Outlook 2003 and higher, but I only tested on Outlook 2007.

The end result looks like this… I am not thrilled about the fact that the attachment name looks so crappy but other than that I quite like it:

image

And this is by the way the setting that needs to be enabled in IE… you probably only want to enable that for trusted sites:

image

Comments
No Comments »
Categories
Uncategorized
Comments rss Comments rss
Trackback Trackback

« Previous Entries

Categories

  • Experiments (4)
  • Interesting (1)
  • MSCRM (1)
  • Programming (60)
  • Rant (3)
  • Saleslogix (34)
  • Tricks (8)
  • Uncategorized (24)

Post History

  • 2010
    • January (3)
    • March (1)
  • 2009
    • March (2)
    • April (1)
    • May (3)
    • June (3)
    • July (1)
    • September (3)
    • October (2)
    • December (5)
  • 2008
    • January (9)
    • February (4)
    • March (9)
    • April (1)
    • May (5)
    • June (8)
    • July (1)
    • August (2)
    • September (1)
    • November (1)
    • December (3)
  • 2007
    • January (3)
    • February (7)
    • March (1)
    • April (3)
    • May (6)
    • June (2)
    • July (1)
    • August (2)
    • September (5)
    • October (3)
    • November (5)
    • December (4)
  • 2006
    • January (2)
    • September (1)
    • November (3)
    • December (4)
  • 2005
    • April (1)

Meta

  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
rss Comments rss valid xhtml 1.1 design by jide powered by Wordpress get firefox