Tuesday, April 24, 2012

Knockout and Click-to-Edit

Knockout is lots of fun to work with.  It uses the MVVM pattern to drastically simplify javascript - you no longer have to mess around with finding elements and updating them or retrieving values from elements.  All you have to do is work with a viewmodel and the DOM is kept in sync automatically.  Here is a small example of one of the fun things I made with knockout recently.

I needed to implement click-to-edit for an element on a page that is bound to a Knockout viewmodel. The idea with click-to-edit is that you see text on a webpage, and when you click it, you are able to edit it, then when you click away, the edit box goes away.  You can see it in action on the Result tab below:



Friday, February 10, 2012

Node.js and the Windows Azure Accelerator for Web Roles

Windows Azure Accelerator for Web Roles
Azure is a great platform to build applications on, and it allows you to scale up very high without a huge investment in infrastructure.  However, what if you want to use Azure to scale down very low?  What do you do if you want to run 20 different small websites, but you don't want to pay $1,200 a month for 40 extra-small web roles (2 per site to ensure availability)?

If that describes what you'd like to do, take a look at the Windows Azure Acclerator for Web Roles (WAAWR). It makes it easy to set up a single Azure web role that can host multiple IIS sites on it - 20, if you want, or more.  You can set up 2 extra small instances of the accelerator role and only be out $60 a month for the instances.  The initial deployment of the Accelerator takes several minutes, as is typical for Azure web role deployments, but deployment of each individual application is done even quicker and using the publishing tools built into Visual Studio.  To see a demo of the WAAWR, watch Cloud Cover Show episode 51.

It's a great tool, go check it out - I won't rehash what's already out there about it.  Instead, I'm going to focus specifically on how to use Node.js with the WAAWR.

Node.Js
A short while ago, Ryan Cromwell, a coworker of mine, asked if Node could be run with the WAAWR.  It seemed like it should, as WAAWR was just setting up IIS sites in Azure automatically, and Node could run in IIS alongside .Net apps, but I wanted to try it out for myself to be sure - and it worked without any problems.  The following are the steps to take to get Node working with the Windows Azure Accelerator for Web Roles.

Thursday, February 2, 2012

LinkPointTransaction.Dll

If anyone else out there ever has work with the LinkPointTransaction.dll assembly, I pity you.  It is not an enjoyable experience.

I've been working on updating an eCommerce site, and before I started working with the payment code, I wanted to run a simple test transaction against LinkPoint so that I could gain familiarity with it before I started changing code around.  Hopefully my story will help others in their struggle.

I first had to sign up for a developer account.  That was fairly simple, and shortly thereafter I received three emails with 3 different accounts - apparently they have 3 different ways you can issue transactions.

1. Web Service API.  This is a SOAP endpoint.
2. Virtual Terminal.  This is where you log into their website and type in transactions manually
3. Connect.  This is where you use LinkPointTransaction.dll to connect to their services

Once I figured out what the 3 different options were, I figured out that the app I was working on used #3.  (Side Note:  If you're starting from scratch, I would recommend checking out the SOAP endpoint so that you don't have to work with assemblies, COM objects, and custom SSL libraries that are several years old).  I then went looking for documentation.  I found the documentation site, but it took me a little while to figure out which PDF was the right one.  I finally figured out that the API user manual was the right one.  I had initially thought that the API user manual would have been the manual for the Web Service API, but I was wrong - the API user manual is for the Connect option.

Monday, January 30, 2012

Code Coverage and Web Tests

Download sample code for this blog entry here.

Code Coverage can be a helpful metric to see how much of your code is hit by the tests you have written.  Although it isn't a way to measure how good your code is, it will at least help you to see what parts of your code have no automated tests written against them.

However, you can run into some difficulties when trying to do code coverage for tests that hit a web site.  Let's look at an example.  Suppose you are creating a web application, and you want to write tests against it.  Some of the tests will be unit tests that test out specific classes in isolation.  Other tests will be integration tests that use HTTP to test the website.  You can see the sample solution structure at the right.

First, we're going to test the SampleLogic class in the Logic project.  Here is the SampleLogic class:

    public class SampleLogic
    {
        public int val;
 
        public SampleLogic(int value)
        {
            this.val = value;
        }
 
        public int Double()
        {
            return val * 2;
        }
    }


Monday, November 14, 2011

Debugging T4 Templates

T4 templates are a great way to write code that writes code.  Huh?  I'm not going to go into it here, so if you want more information, see Scott Hanselman's post on T4 templates, as well as Oleg Synch's posts.

One of the pain points when writing T4 templates is with debugging them.  Sometimes you'll save a .tt file and get some obscure error message that is impossible to figure out, and you have no clue what part of your T4 file is messed up.

It is possible to debug them, and Oleg provides some great pointers.  Here is my summary of how to get it working:


1. Make a registry edit as follows:
     If you're on a 32 bit OS:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\DbgJITDebugLaunchSetting - Set to 0x2
      If you're on a 64 bit OS:

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\.NETFramework\DbgJITDebugLaunchSetting - Set to 0x2
(if you don't make this change, you can get debugging to work, but Visual Studio will be hung after you finish debugging)

2. Reboot
(otherwise the registry change won't get picked up)

3. Set debugging to true at the top of your .tt file:
<#@ template language="C#v3.5" debug="True" #>

4. Force the debugger to launch
System.Diagnostics.Debugger.Launch();


5. Put breakpoints in your code
System.Diagnostics.Debugger.Break();

6. Make a change to and then save your T4 file


When you save or run your T4 file, it should now pop up a window asking you if you want to debug it using a new instance of Visual Studio.  This second instance should allow you to step through the execution of the T4 file so that you can see where your code is broken.

Tuesday, October 11, 2011

Constructor vs. ClassInitialize

I recently ran across some code in a test class where I saw that the class was initialized in a constructor rather than in a method with the ClassInitialize method, and it made me wonder - what's the difference?

The goal is that we have a test class with several test methods, and we have some initialization code that is common to all of the test methods.  We want the initialization code to run only once for the entire set of tests, not once per test.

One way to do this is to put the initialization code in the constructor, like this:

        public MyTestClass()
        {
            var x = 5;
            x.Should().Be(4);  // Fluent Assertions
        }

Another way is to create a static method with the ClassInitialize attribute, like this:

        [ClassInitialize]
        public static void Init(TestContext testContext)
        {
            var x = 5;
            x.Should().Be(4);  // Fluent Assertions
        }

So what's the difference?

Wednesday, July 20, 2011

Sendoid

I've been keeping an eye out for a simple way to share a large file (1-5 GB) to another person over the internet privately, and I finally found something that I like.   Sendoid.

The uploader and downloader can use it either from the web page or from the client app they provide.  However, if you use the client app you get the ability to resume a transfer in the event that it's interrupted, which is very important for large files.


You can have multiple people downloading from you at the same time, and when you're done, remove the file from the list or close the app.  Very simple, no registration or signup required.

Thanks, Sendoid!


http://sendoid.com

Wednesday, June 1, 2011

Postback Failing

I ran into a strange problem this week on a client's .Net site I was working on.  There were two pages where postbacks were being completely ignored.  If you clicked on anything that required codebehind or postbacks to work, such as the login/logout button, the save button, etc., an HTTP POST would occur (verified by Firebug), but the server would ignore the POST and serve up the page as if you did a GET.

Interestingly, it worked great on IE and Safari, but failed on Chrome and Firefox.  What was even stranger was that everything worked properly when debugging in Visual Studio, but the failure occurred when the site was deployed to IIS.

So the failure occurred only when running Firefox or Chrome against IIS.  The same website with IE and Safari worked great, and the same website on Cassini worked great in any web browser.

Hmm...

I ended up using WinDiff to compare the HTML output differences between what was served up from Cassini (Visual Studio during debugging) and IIS.  There was one line that was different:

On Cassini:
<form name="aspnetForm" method="post" action="/default.aspx?" id="aspnetForm">

On IIS
<form name="aspnetForm" method="post" action="/" id="aspnetForm">

Monday, May 23, 2011

Sharepoint 2010 Custom Workflow Activites

(If you're impatient, you can see the code before reading the article)

Sharepoint has a workflow engine, and you can develop workflows for Sharepoint using either Sharepoint Designer or Visual Studio.  Today I'm going to focus on workflows that are created using Sharepoint Designer.  A step in a workflow, such as "Copy item from List A to List B", is called an Activity.

Sharepoint Designer comes with a number of activities out of the box, but you can use Visual Studio to create custom activities that are surfaced to Sharepoint Designer.  For example, if you saw the existing workflow "Copy item from List A to List B", but it wasn't quite what you wanted, you could create a custom activity for "Copy item from List A to List B and List C", and that activity would then show up in Sharepoint Designer when creating a workflow.

Lets dig into how you can create a custom activity for Sharepoint Designer using Visual Studio.


Thursday, November 4, 2010

Generics

Ok, so I'm a little late to the game here.  Generics came out with .Net 2.0, which was released back in 2005.  I've used them a good bit, but today I was asked what particular advantages Generics had over what was available in .Net 1.1, and, not having worked with .Net 1.1, I wasn't really sure.  So I did some digging and figured I'd share my results, for what it's worth.

In .Net 1.1, if you wanted to have a collection of objects, such as a linked list, a simple array-like list, a queue, or, in fact, if you wanted to make a class that made use of some object internally, but you wanted to make your data structure available to multiple internal types, you had several options, but they all had some significant limitations. 

Lets look at some examples.  Suppose, for instance, you wanted to create a queue class, where you could have a queue of int, string, or any other type such as car, pet, or person (other classes you need to define elsewhere).   Here are your options in .Net 1.1:

1. You could just use the generic 'object' type, as seen below.  The big disadvantage here is that you have to be very careful adding and removing items, because you could easily add an object of the wrong type and you wouldn't know until run time when you hit an invalid cast exception.

    public class Queue
    {
        public void enQueue(object o) { ... }
        public object deQueue() { ...  }
    }


2. You could create a intQueue class, a strQueue class, and so on.  The intQueue class would look like the code below.  You would get type safety, but the big disadvantage here is that anytime you want to create a queue of a new object, you have to create a new queue class, such as petQueue, carQueue, personQueue.  That's a lot of duplicated code, which increases maintenance time and you could easily end up with inconsistencies between your queue classes. 

    public class intQueue
    {
        public void enQueue(int i) { ... }
        public int deQueue() { ... }
    }




Enter .Net 2.0 and Generics.  In .Net 2.0, you can define your generic queue class as such:

    public class Queue<T>
   {
        public void enQueue(T t) { ... }
        public T deQueue() { ... }
   }

Notice that we use the generic placeholder of T.  T is whatever class you want it to be, but T is defined when you actually declare / instantiate an object of the Queue class.  Here is how you would make use of the class:

    Queue<int> myIntQueue = new Queue<int>;

Notice how we put 'int' in between the < > signs.  This signifies to the compiler that we are creating a queue of type int.  So if we take the queue of int and try to add a string to it, like this:

    myIntQueue.enQueue("ABC");

The compiler will throw an error, telling us that we can't add a string to a queue of int.

So there you go.  Generics give us the ability to generically (i.e., in a template sort of way) declare data structures that encapsulate other types without knowing what the type is that we are encapsulating, and they provide compile time type safety.

Saturday, October 16, 2010

Development Tools

I'm setting up a new development environment, and I thought I'd share the list of tools and technologies I'm putting into place there.  Please add any you'd recommend in the comments.  Everything here is free except the OS, SmartSVN, and RedGate SQL Source Control.
 
Operating System:   Windows Server 2008 R2 x64 ($$)
Application Server:  IIS 7 with .Net 4.0
Database Server:  SQL Server 2008 Developer Edition R2 x64 ($35)
(yes, that's right - Microsoft has a fully-featured version of SQL server for development that is dirt cheap)

Installation Tools used:
  • Web Platform Installer - tool to help install IIS, .Net, MVC, and other components
  • NuPack - helps to add extra assemblies / web.config merges, etc. for third party DLLs for your project

Thursday, October 14, 2010

Migrating Umbraco from MySQL to MSSQL

So you've been running the Umbraco CMS for a while on MySQL, but you've been told you need to move it to Microsoft SQL Server. Does it sound intimidating? It doesn't have to be. With the SQL script and instructions in this post, it's not too bad. First, let's start with an overview of the process.

Overview:
1. Use the Umbraco installer scripts to create blank Umbraco tables on your SQL server
2. Set up a linked database in SQL Server to your MySQL database
3. Migrate the data (using my script below)
4. Update your web.config to point to the new database

Tuesday, September 7, 2010

SVN Trouble

(If all you want to do is solve the same problem, you only need to add a Timeout 1800 line to your c:\program files\visualsvn server\conf\httpd-custom.conf file and restart Visual SVN server - you can, of course, adjust the number of seconds as you like)

First off, if you are doing any kind of software development and you aren't using source control of some kind, you are missing out big time. I highly recommend source control. I use Visual SVN for the server and Smart SVN for the client, but there are lots of other great options out there. OK, on to the good stuff.

We have purchased licenses for RedGate's SQL Source Control, which allows you to use SQL Manager to check in SQL objects into a SVN repository. Very cool. I had been using this for a short while, and I soon saw this error when trying to scan for changes:

Could not read chunk Size: secure connection truncated

Toodledo

Having recently changed over to using a Droid Incredible, I'm having to rework some of my productivity tools to work with the Incredible. Part of that was finding a new GTD / Todo list app.

I had used OmniFocus on my Mac / iPod Touch, but ever since the iOS 4 upgrade, the only way to get it to sync is to wipe out the OmniFocus database on the iPod Touch and start over (My guess is that they updated their software to work with multitasking, and as a result it breaks with devices, like the iPod Touch 2G, that don't support multitasking on iOS4). Not a great solution. After waiting several months for a fix to this, I've given up and I looked around for something similar for my Droid. I strongly considered Remember The Milk, but the Mobile side of that app was rather weak.

I ended up settling on Toodledo. It needs some work on the ability to set custom filters, but overall it does the job I need it to do and it even has a widget for the Droid that shows my 'hotlist,', which is a huge help for me to quickly see the next 10 items on my list, color coded by priority.

Will I stick with it long term? I'm not sure. But so far I like it. I did spend a couple of hours transferring all of my todo items from OmniFocus to Toodledo the other night, so I'm committed to trying to make it work.

I use it to keep track of my grocery list, and I walk around with it at Kroger like a true geek, checking things off as I put them in my cart - my wife has had several good laughs at me as a result.

But I also use it to keep track of when to do maintenance on the cars and house, errands I need to run, etc. It's great to have a GTD tool that works on the web and syncs to my Droid.

Friday, July 2, 2010

Sharepoint 2007: 'Only their own' permission on Document Library - Update!

OK, after getting tired of manually setting this using Sharepoint Manager 2007, I decided to put together a solution. This solution provides a nice drop-down on the Document Library 'Security' menu like this:



And when you click on the 'Read/Write Security' link, you'll see this:



It looks and acts just like the page for standard lists - but this one also works for document libraries.

Download the solution from codeplex at http://moresharepoint.codeplex.com

Wednesday, June 30, 2010

Sharepoint 2007: Picture Library limitations

In working with the Picture Library in Sharepoint 2007, I've run across 2 limitations:

1. No web part connections. For some reason Microsoft decided that Picture Libraries would never need web part connections. It's greyed out, as seen here:



2. Picture Libraries do not have the option to give users permissions to only view the pictures they created (and not view pictures that other users created). You can do this on non-document, non-picture library lists. Here is what it looks like on the 'Advanced Settings' page for a list:


But you don't get that on a picture library.

Thursday, June 17, 2010

Sharepoint 2007: 'Only their own' permission on Document Library

In Sharepoint 2007 you have a great permission you can set: 'Only their own'. This allows someone to view and edit only the items that they have created, and not other items that other people have created. Administrators of the site can see everything, of course.

This is easy to set on a List under the list settings. However, if you are using a Document Library, the setting is nowhere to be found. It is there, but it's hidden.

To change this setting, you can either write some code, as described here, or you can do the equivalent with Sharepoint Manager (make sure you get the 2007 version). Open Sharepoint Manager (while logged into your Sharepoint Server), drill down to the Document Library you are interested in, and you should see the 'WriteSecurity' setting. Change it to 2 to effectively set it to 'only their own.'

However, keep in mind that this only changes what the user can do via the UI. If they get to the list data via web services or some other method, this won't prevent them from getting at other people's data.

Friday, May 28, 2010

Passing a table using Nav Automation

I just had someone comment on my previous blog post asking how to pass a table using Nav Automation. Great question.

You can pass complex objects using Nav Automation, but only ones that you define (or that have been explicitly written to work with COM). This means that you can't pass a DataTable object, but you can create your own implementation of a DataTable object and pass it.

In your complex object, you have to explicitly define every property and method that you need to call on that complex object and surface those properties and methods through COM.

Tuesday, March 30, 2010

x does not contain a definition for y

I just ran across this problem... again... and because I didn't note the solution last time I had to figure it out again. It's so simple - but so hard to remember. Let's hope that next time I hit this I'll at least remember that I blogged about it. Here we go.

I'm building a web site, and it makes use of some DLLs that come from some C# libraries I've built. Over the course of time, I've added some functions to the libraries that I use in my website. And the website picks up the new functions and everything is great.

Then one day I come along and try to build the website in Visual Studio, but it gives me an error because it says that one of the newer functions in my library is missing. Here's the error:
className does not contain a definition for functionName
Hmm. I check the other project (which lives in the same solution), and the function is there. So I go to my web control, which is generating the error, I right click on the function name, and click 'Go to Definition', and sure enough, it jumps right to the function.

After a couple of hours of removing pieces and parts from different projects and trying to narrow things down, all without any avail, a thought finally hits my brain. The GAC!

Sure enough, an older version of my library somehow ended up in the GAC. I removed it from the GAC, then everything built just fine.

FYI, to remove a library from the GAC, browse in windows explorer to c:\windows\assembly, right click on the dll you want to remove, and click 'uninstall.'

Tuesday, October 20, 2009

Linq causes the Setup project to fail when building

All of a sudden, my setup project stopped building. It was very strange - when I tried to build it, it would say that the build failed, but it gave no errors. Nothing anywhere.

Thankfully, I'm not the first person to have run across this. It turns out that adding Linq to one of the projects in my solution caused this failure - it appears to be a bug in Visual Studio.

The solution appears at Microsoft's Connect site (be sure to look in the 'Workarounds' tab) - https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=317870

Here is what Daren wrote:

1. Close VS 2008.
2. Open the project file containing the LINQ To SQL item in Notepad.
3. Remove the following lines:
<ItemGroup>
<Service Include="{3259AA49-8AA1-44D3-9025-A0B520596A8C}" />
</ItemGroup>

The Setup Project will now build successfully. However, if you double-click the DBML file to open the designer in VS 2008 the Setup Project will stop building again. The above lines do *not* get re-added to the project file but the Setup Project will stop building anyway. Just restart VS 2008 and it will work again -- until you open the DBML designer again. Once the Setup Project fails due to this problem it will never build successfully until after you restart VS 2008.

Friday, October 9, 2009

OOO, C#, and C/AL

I've been working with building some web services built on the Microsoft Dynamics Nav 2009 web services platform. Microsoft is doing something fairly slick here - you write code in the C/AL language, save it as a Nav Object, expose it as a web service, and the web services layer automatically translates the C/AL to C# and runs it in .Net. This allows you to debug the code using visual studio, among other things.

But it's not perfect. Sometimes it makes mistakes.

I had this line of code in C/AL
IF AVSetup."Sub Award Dimension No." - 1 IN [1..8] THEN BEGIN
Which auto-translated to this in C#:
if(aVSetup.Target.
GetFieldValueSafe(90, NavType.Integer).ToInt32()-(1 >= 1 && 1 <= 8))
Oops. The order of operations got mixed up. The C/AL code subtracts one, then sees if the value is between 1 and 8. The C# sees if 1 is between 1 and 8 (getting a boolean value) and then tries to subtract the boolean value from the number. C# then gives this error:
Operator '-' cannot be applied to operands of type 'int' and 'bool'
Thankfully, the solution for this is simple. All you have to do is use extra parentheses to explicitly set the Order of Operations.
IF ((AVSetup."Sub Award Dimension No." - 1) IN [1..8]) THEN BEGIN

Monday, October 5, 2009

SQL Reporting Services, Sharepoint, and Firefox

I ran into a problem when I tried to use Firefox to view a SQL report on Sharepoint. In IE it works fine, but when it hits firefox, a 'display: block-inline' CSS style set on a table buried way down in the code caused firefox to do this:


The report is squeezed into a little IFrame, and you don't even get a vertical scrollbar. Sure, you can right-click on it and get your browser to show that IFrame as the main page, but that's not a good solution for end users. So I set about trying to figure out how to take care of this.

I found all kinds of helpful information about the problem. Lots of people had solutions, but none of them worked for me. Here is some of what I found:

Part of the problem is that I'm using SQL Reports with Sharepoint in Sharepoint Integration mode, whereas they seem to be using standable SQL Reports.

So I started digging. I used Firebug, a great tool by the way, to dig around the DOM to find out what was causing the problem. After drilling down through countless nested tables, I discovered that there was one control in particular that was causing the trouble:
<table id="m_sqlRsWebPart_ctl06_ctl13" cellspacing="0" cellpadding="0" style="overflow: auto; display: inline-block; width: 100%; height: 100%;">
In particular, the 'display:inline-block' didn't get along so well with firefox. When I removed that with Firebug, the report showed up just great. But the problem is that this HTML code is actually assigned by Microsoft.ReportingServices.Sharepoint.UI.WebParts.DLL (or by something that it calls), and modifying the DLLs isn't really an option.

After searching around, I found out that you can have a CSS declaration in a stylesheet override an element CSS declaration. These guys clued me in to this gem:
So I went to the reports server and went into the 12 hive at 12\TEMPLATE\LAYOUTS\ReportServer\RSViewerPage.aspx, and inside the HEAD element I added this:
<STYLE type="text/css">
table#m_sqlRsWebPart_ctl06_ctl13[style] {
display: table !important;
}
</STYLE>

Et Voila, it works! This chunk of CSS overrides what the DLL puts on the table element and changes the display property from inline-block to table. That then causes Firefox to behave properly and show the report.

Well, mostly. The other thing you need to do is open your report designer, open the report, and create a blank textbox that spans the width of the report. That will make sure that nothing on the right side of the report gets cut off.

Tuesday, March 10, 2009

Sharepoint Thumbnails and AssetUploader.aspx

We recently had a consultant build a web part for us that displayed thumbnails from images in a document library. In digging around in the code they gave us I discovered that they used a page built into Sharepoint called AssetUploader.aspx (in 12\TEMPLATE\LAYOUTS\AssetUploader.aspx, available off any web at _layouts\AssetUploader.aspx). This page is a rather handy. It's specifically designed to take a URL of an available image, resize it, and spit out a thumbnail. You can use it in any page or web part in Sharepoint by creating an IMG tag like this:
<img src="http://www.blogger.com/_layouts/AssetUploader.aspx?Size=Medium&ImageURL=/mysitecollection/mysite/MyImagesDocumentLibrary/IMG1234.jpg" alt="My Image" />
If you use a size of 'Small', it will make the largest dimension of the image 70 pixels, and if you use anything other than small (Medium, Large, etc.), it will make the largest dimension of the image 140 pixels.

It worked wonderfully in our test environment, but once we brought it into production, it broke. All we saw were little generic white document icons - no thumbnails. Yikes!

I turned, of course, to google, and I found a very helpful post from Bob Moore at PointBridge, where he explained the problem and provided some code as a solution. His code was tremendously helpful, but didn't get me all the way there, so I went to reflector myself to try and disassemble and fix it on my own.

However, interestingly, reflector couldn't disassemble all of it. The main class I was interested in was Microsoft.SharePoint.Publishing.Internal.CodeBehind.AssetThumbnailerPage. So I opened it in reflector to find this message when I tried to disassemble the OnLoad event:
// This item is obfuscated and cannot be translated.
I soon found that while Reflector couldn't show me the C# code for that function, I could look at the IL code for it (to do this, click on the language drop down in the toolbar and change it from C# or Visual Basic to IL). I'd never seen this before, but once I found Microsoft's IL reference, I was able to decipher and disassemble it by hand back into C#.

Here is the solution I ended up with that works well with AAM. I created a simple ASPX page containing only this:
<%@ Assembly Name="MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=0123456789abcdef"%>
<%@ Page Language="C#" Inherits="MyNamespace.MyAssetThumbnailerPage" %>

And here is the code for the MyAssetThumbnailerPage class, which I included in a DLL that gets deployed via a feature to the GAC:
using System;
using System.Drawing;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Security.Permissions;
using System.IO;
using System.Globalization;
using System.Drawing.Imaging;
using System.Web;
using System.Collections;

namespace MyNamespace
{
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
public partial class MyAssetThumbnailerPage : UnsecuredLayoutsPageBase
{

protected override void OnLoad(EventArgs e)
{
int Size;
SPWeb web = null;
SPSite site = null;
try
{
string ImageURL = Request.QueryString["ImageURL"].ToString();
if (Request.QueryString["Size"].ToString() == "Small")
{
Size = 70;
}
else
{
Size = 140;
}

Uri uri = new Uri(SPContext.Current.Web.Url);
string url = uri.Scheme + "://" + uri.Host + ImageURL;
Uri uri2 = new Uri(HttpContext.Current.Request.Url, url);

site = SPContext.Current.Site;
site = new SPSite(SPContext.Current.Web.Url);
SPSecurity.CatchAccessDeniedException = false;
web = site.OpenWeb();
SPFile file;
file = (SPFile)web.GetFileOrFolderObject(uri2.AbsoluteUri);

SPSecurity.CatchAccessDeniedException = true;

if (file == null)
{
Server.Transfer("/_layouts/IMAGES/ICGEN.GIF");
}

if (!IsImageUrl(file.Url))
{
Server.Transfer("/_layouts/IMAGES/" + file.IconUrl);
}

byte[] fileBytes = MyAssetThumbnailer.CreateThumbnail
(file.OpenBinary(),Size);


Response.OutputStream.Write(fileBytes, 0, fileBytes.Length);

base.OnLoad(e);
}
catch (Exception ex)
{
base.OnLoad(e);
}
finally
{
web.Close();
web.Dispose();
site.Close();
site.Dispose();
}
}

private static bool IsImageUrl(string fullUrlToTest)
{
if (!string.IsNullOrEmpty(fullUrlToTest))
{
string str = fullUrlToTest;
int index = fullUrlToTest.IndexOf
("?", StringComparison.Ordinal);

if (-1 == index)
{
index = fullUrlToTest.IndexOf
("#", StringComparison.Ordinal);

}
if (-1 != index)
{
str = fullUrlToTest.Substring(0, index);
}
string str2 = string.Empty;
if (!string.IsNullOrEmpty(str))
{
index = str.LastIndexOf(".", StringComparison.Ordinal);
if (-1 != index)
{
str2 = str.Substring(index);
}
}
//ULS.SendTraceTag(ULSTagID.tag_6osa,
//ULSCat.msoulscat_CMS_Publishing, ULSTraceLevel.Medium,
//"Verifying that the extension [%s] of URL ['%s' - full: " +
//"'%s'] is a recognized image type.",
//new object[] { str2, str, fullUrlToTest });

if (string.IsNullOrEmpty(str2))
{
return false;
}
ArrayList list = new ArrayList(new string[] { ".bmp", ".gif",
".jpeg", ".jpg", ".jpe", ".png" });

return list.Contains(str2.ToLowerInvariant());
}
return false;
}



}

internal class MyAssetThumbnailer
{
private MyAssetThumbnailer()
{

}

public static byte[] CreateThumbnail(byte[] inputBuffer,
int thumbMaxDimension)

{
MemoryStream stream = new MemoryStream(inputBuffer);
Bitmap bitmap = new Bitmap(stream);
Size imageSize = bitmap.Size;
Size size2 =getThumbnailDimensions(imageSize, thumbMaxDimension);
if (size2 == imageSize)
{
object[] objArray = new object[] {
thumbMaxDimension.ToString(CultureInfo.InvariantCulture),
imageSize.Width.ToString(CultureInfo.InvariantCulture),
imageSize.Height.ToString(CultureInfo.InvariantCulture),
inputBuffer.Length.ToString(CultureInfo.InvariantCulture)
};

//ULS.SendTraceTag(ULSTagID.tag_6osl,
//ULSCat.msoulscat_CMS_Publishing, ULSTraceLevel.Medium,
//"No thumbnail made - thumbnail size [%s] is greater " +
//"than image dimensions[%sx%s] bytes: %s", objArray);

return inputBuffer;
}
object[] data = new object[] {
size2.Width.ToString(CultureInfo.InvariantCulture),
size2.Height.ToString(CultureInfo.InvariantCulture),
imageSize.Width.ToString(CultureInfo.InvariantCulture),
imageSize.Height.ToString(CultureInfo.InvariantCulture),
inputBuffer.Length.ToString(CultureInfo.InvariantCulture)
};

//ULS.SendTraceTag(ULSTagID.tag_6osm,
//ULSCat.msoulscat_CMS_Publishing, ULSTraceLevel.Medium,
//"Generating thumbnail with dimensions[%sx%s] for image with " +
//"dimensions[%sx%s] bytes: %s", data);

return imageObjectToBytes(
bitmap.GetThumbnailImage(
size2.Width,
size2.Height,
new System.Drawing.Image.GetThumbnailImageAbort
(MyAssetThumbnailer.thumbnailCallback),
IntPtr.Zero
)
);

}

private static Size getThumbnailDimensions
(Size imageSize, int thumbMaxDimension)

{

if ((imageSize.Height > thumbMaxDimension) ||
(imageSize.Width > thumbMaxDimension))

{
SizeF ef = new SizeF((float)thumbMaxDimension,
(float)thumbMaxDimension);

if (imageSize.Height <= imageSize.Width)
{
ef.Height = (imageSize.Height * thumbMaxDimension) /
imageSize.Width;

}
else
{
ef.Width = (imageSize.Width * thumbMaxDimension) /
imageSize.Height;

}
if (ef.Width <>
{
ef.Width = 1f;
}
if (ef.Height <>
{
ef.Height = 1f;
}
if (ef.Width > thumbMaxDimension)
{
ef.Width = thumbMaxDimension;
}
if (ef.Height > thumbMaxDimension)
{
ef.Height = thumbMaxDimension;
}
return Size.Ceiling(ef);
}
return imageSize;
}

private static byte[] imageObjectToBytes
(System.Drawing.Image imageObject)

{
MemoryStream stream = new MemoryStream();
imageObject.Save(stream, ImageFormat.Jpeg);
return stream.ToArray();
}

private static bool thumbnailCallback()
{
return false;
}
}
}

Wednesday, December 3, 2008

NAV Automation Object

It took me a good while, but I finally managed to get my first automation object created, compiled, and made visible in NAV. Stefano Demiliani provided most of the guidance that I needed on this, as well as a few postings on mibuso, but there were a few other tricks I picked up on in the process that I figured I'd share here. Also, since I couldn't find one anywhere else, I figured I'd post a complete Visual Studio sample project.

You can download the sample project at http://bitsofinsanity.blob.core.windows.net/public/NavAutomation.zip

Please note that I am far from an expert on COM objects - I figured out just enough to get my code to compile and work, but it's highly likely that my work is suboptimal, so if anyone has any ideas for improvement, please share them.

I suppose I should explain what I'm talking about. Microsoft Dynamics NAV is an ERP system that contains and manages our Finances, Accounting, HR, Events, and more. It has tables of data (backed by SQL Server), forms to display the data, reports, and more. Each of these object types can have code behind it controlling what it does. However, the extensibility is limited to using COM objects, which are called 'Automation Objects' in NAV. But so long as you can create a COM object properly, you can insert it into a table or form and, through that COM object, you can do whatever the .Net framework allows you to do.

In working to create a COM object, I already had a class and and assembly that performed the functionality I wanted. I wanted to wrap it with COM so that I could see my class and its methods and functions in Nav. I first tried by adding the COM tags to my existing class, and I could kindof-sortof get it to work, but it never really worked completely. So I ended up creating a separate assembly and a separate class that contained the COM wrapper. All it does is call the real class, which does the hard work (the sample code doesn't call any other classes, but you could easily change it to do so).


Notes on creating an Automation Object

Your assembly will need to be strongly named. To do this:
  1. Right-click on your project name, click 'Properties', click the 'Signing' tab on the left
  2. Check the 'Sign the assembly' checkbox'
  3. In the 'Choose a strong name key file' drop down, choose 'New'. Give it a file name (anything you want), and uncheck the 'Protect my key file with a password' checkbox. Click 'OK'.

Please note the Post-build events in the sample code (under the project Properties). Make sure the paths to gacutil and regasm are correct. I don't know why, but for some reason I found I had to run the regasm and gacutil commands twice to make the COM object visible in Nav, hence the duplication of the lines below. These should register it on your development box so that you can see it in Nav. If you want to use the automation object elsewhere, you'll have to register the assembly in a similar manner on the other client machines. The post build events are:
SET GACUTIL="C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin\gacutil.exe"
SET REGASM="C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\regasm.exe"


%REGASM% $(TargetFileName) /tlb:NavAutomation.tlb

%GACUTIL% /i $(TargetFileName)


%REGASM% $(TargetFileName) /tlb:NavAutomation.tlb

%GACUTIL% /i $(TargetFileName)

After registering the assembly and type library, open Nav, open an object, and create a variable of type 'Automation.' For the subtype, click on the '...' then click on the drill-up arrow by 'Automation Server.' That will list all the COM objects loaded in your system, and you should be able to find your custom Automation object by the Assembly name (for the sample code, it should be called NavAutomation).


Be sure to create your own GUIDs for your project. The GUIDs you need to change are the ones on the interface and class definition in the cs file.

The sample project is built in Visual Studio 2008 with C# on .Net 3.5, and I tested using it on Nav 5.0 SP1, all running on Windows Server 2003 with SP2. It should work with other versions of Visual Studio, .Net, Nav, and Windows, but I haven't tested those out.

If you have an overloaded function in C#, the COM object representation in Nav will change it around a little. For example, if you have overloaded a function called 'test' with 3 different definitions, it will show up as 'test', 'test_2', and 'test_3.' This is done automatically. See the Nav symbol menu to find out what the names actually come up as.

There is no automatic type conversion between Decimal values in .Net and Decimal values in Nav, so you'll have to do a work around if you need to use decimal for a parameter or return value. I used a string object, casting it appropriately on both sides.

The Nav side can't handle constructors that require parameters. To get around this, you can create a constructor with no parameters and a function called 'init' that handles the actual construction logic.