Olav Aukan Getting information off the Internet is like taking a drink from a fire hydrant…

12Oct/10

SharePoint does not always play nice with comments

Some days ago I was tasked with removing the "Add to my colleagues" button from the My Profile page in SharePoint. This is a one-off page located at the root of the My Site site collection named Person.aspx. So I fired up SharePoint Designer (yes I know, SPD is a great evil, but for these kinds of changes it's really the easiest way) and looked at the file. It turns out that the "Add to my colleagues" button is a control in the PlaceHolderMiniConsole placeholder, so I commented it out and saved the page.

<asp:Content ContentPlaceHolderId="PlaceHolderMiniConsole" runat="server">
	<SPSWC:MiniConsole runat="server" id="miniConsole">
	<SPSWC:SPSRepeatedControls id="RptControls" runat="server" HeaderHtml="" BeforeControlHtml="<td class='ms-toolbar' nowrap='true' ID='_spFocusHere'>" AfterControlHtml="</td>" SeparatorHtml="<td class=ms-separator>|</td>">
		<Template_Controls>
			<SPSWC:ReturnToSiteButton runat="server" id="TBBReturnToSite" class="ms-toolbar" ShowImageAndText="false"/>
			<!--<SPSWC:AddToColleaguesButton runat="server" visible="false" id="TBBAddtoColleagues" class="ms-toolbar" ShowImageAndText="false"/>-->
			<SPSWC:AsSeenBy runat="server" id="ddlAsSeenBy" SelectionMode="Single" autopostback=true/>
		</Template_Controls>
	</SPSWC:SPSRepeatedControls>
  </SPSWC:MiniConsole>
</asp:Content>

Much to my surprise nothing happened and the control was still rendered. I tried to revert Person.aspx to it's definition and do the changes again, I tried disabling the cache, I tried iisreset, but nothing worked. In the end I deleted the control instead of commenting it out. That worked...

A colleague of mine had a different problem. His content type was suddenly missing a site column. He was being a good developer and deploying it with XML, just like Microsoft tell you to do. I'm sure we all know about the MANY issues with this approach. So many in fact that I'm almost considering using the object model to add my site columns and content types on my next project, but that's a different subject altogether... We looked at the XML and couldn't see anything wrong with it:

<ContentType ID="0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D000cba91cafa2b473e85a2e018ed328699"
               Name="InsideBaseArticle"
               Group="Inside Portal"
               Description="Base Content Type for publishing content in the Inside Portal"
               Inherits="TRUE"
               Version="0">
    <FieldRefs>
      <FieldRef ID="{6440A642-CE85-4AF3-BFF7-1900D5095D87}" />   <!-- Tax Note Field -->
      <FieldRef ID="{D5F62894-71DA-49E1-88CE-A8FBBCBFCE2F}" Name="Via" />  <!-- Tax Field -->
      <FieldRef ID="{9DFA6A86-70E7-4470-B3FD-4D68D1FDD1A5}" Name="Summary"  Required="FALSE" />
      <FieldRef ID="{5a14d1ab-1513-48c7-97b3-657a5ba6c742}" Name="AverageRating" />
      <FieldRef ID="{b1996002-9167-45e5-a4df-b2c41c6723c7}" Name="RatingCount" />
    </FieldRefs>
</ContentType>

Turns out it was the comments in the FieldRefs section. Removing them and deploying again resolved the issue.

Moral of the story? SharePoint doesn't always play nice with comments...

Tagged as: 2 Comments
31Aug/10

Creating an ActiveX control in .Net using C#

A while back I had a client request that I write an ActiveX control for use on their corporate intranet. I had never done this before, and most of the examples I could find online were either really old, incomplete or based on using C++ and MFC. It's safe to say that my C++ skills are not quite up to the job, so for me it was really a requirement to be able to do this with C#. It took me about an hour or two to write the code for the control, but it took almost three days to successfully package and deploy it as a .cab file... So to save others from wasting their time like I did, I'll document my findings in this post.

Steps

  1. - Create a new Class Library project in Visual Studio
  2. - Create a new class that inherits from UserControl
  3. - Create a new interface that exposes the controls methods and properties to COM interop
  4. - Make the control class implement the new interface
  5. - Mark the control as safe for scripting and initialization
  6. - Create a .msi installer for the control
  7. - Package the control in a .cab file for web deployment
  8. - Initialize and test the control with JavaScript

1. Create a new Class Library project in Visual Studio

I'm using Visual Studio 2008, but other versions should work as well.

  1. After starting Visual Studio click File -> New -> Project and select Class Library under C#.
  2. Call the project 'AxControls' and click OK.

2. Create a new class that inherits from UserControl

  1. Rename 'Class1.cs' to 'HelloWorld.cs', making sure to rename the class name as well.
  2. Add a project reference to System.Windows.Forms.
  3. Make the HelloWorld class inherit UserControl.

3. Create a new interface that exposes the controls methods and properties to COM interop

  1. Right click the project in Visual Studio and click Add -> New Item.
  2. Select 'Interface' from the list of components, name it 'IHelloWorld.cs' and click Add.
  3. Edit the 'IHelloWorld.cs' file so it looks like this:
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Runtime.InteropServices;
    
    namespace AxControls
    {
        [ComVisible(true)]
        [InterfaceType(ComInterfaceType.InterfaceIsDual)]
        [Guid("41E85D5D-C57A-4386-B722-4031D0B1E1B7")]
        public interface IHelloWorld
        {
            string GetText();
        }
    }
    

We now have a COM visible interface with a single method 'GetText()'.

[ComVisible(true)] makes the interface visible to COM.
[InterfaceType(ComInterfaceType.InterfaceIsDual)] sets the COM interface type to Dual, see InterfaceTypeAttribute Class on MSDN.
[Guid("41E85D5D-C57A-4386-B722-4031D0B1E1B7")] let's us manually assign a GUID to the interface. Use guidgen.exe to generate your own.

4. Make the control class implement the new interface

Make the HelloWorld class implement the IHelloWorld interface and have the GetText() method return a string of your choice. This is what the file might look like:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace AxControls
{
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    [Guid("1FC0D50A-4803-4f97-94FB-2F41717F558D")]
    [ProgId("AxControls.HelloWorld")]
    [ComDefaultInterface(typeof(IHelloWorld))]
    public class HelloWorld : UserControl, IHelloWorld
    {
        #region IHelloWorld Members

        public string GetText()
        {
            return "Hello ActiveX World!";
        }

        #endregion
    }
}

We now have a COM visible control that implements the IHelloWorld interface.

[ComVisible(true)] makes the control visible to COM, see ComVisibleAttribute Class on MSDN.
[ClassInterface(ClassInterfaceType.None)] indicates that no class interface is generated for this class, see ClassInterfaceType Enumeration on MSDN.
[Guid("1FC0D50A-4803-4f97-94FB-2F41717F558D")] let's us manually assign a GUID to the control, see GuidAttribute Class on MSDN. Use guidgen.exe to generate your own.
[ProgId("AxControls.HelloWorld")] is a "user friendly" ID that we'll use later from JavaScript when initiating the control, see ProgIdAttribute Class on MSDN.
[ComDefaultInterface(typeof(IHelloWorld))] sets IHelloWorld as the default interface that will be exposed to COM, see ComDefaultInterfaceAttribute Class on MSDN.

5. Mark the control as safe for scripting and initialization

By default IE will not allow initializing and scripting an ActiveX control unless it is marked as safe. This means that we won't be able to create instances of our ActiveX class with JavaScript by default. We can get around this by modifying the browser security settings, but a more elegant way would be to mark the control as safe. Before you do this to a "real" control, be sure to understand the consequences. I found an ancient (1996) MSDN article that explains this here. We will mark the control as safe by implementing the IObjectSafety interface.

  1. Right click the project in Visual Studio and click Add -> New Item.
  2. Select 'Interface' from the list of components, name it 'IObjectSafety.cs' and click Add.
  3. Edit the 'IObjectSafety.cs' file so it looks like this:
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Runtime.InteropServices;
    
    namespace AxControls
    {
        [ComImport()]
        [Guid("CB5BDC81-93C1-11CF-8F20-00805F2CD064")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        interface IObjectSafety
        {
            [PreserveSig()]
            int GetInterfaceSafetyOptions(ref Guid riid, out int pdwSupportedOptions, out int pdwEnabledOptions);
    
            [PreserveSig()]
            int SetInterfaceSafetyOptions(ref Guid riid, int dwOptionSetMask, int dwEnabledOptions);
        }
    }
    
  4. Make the HelloWorld class implement the IObjectSafety interface. The end result should look something like this:
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows.Forms;
    using System.Runtime.InteropServices;
    
    namespace AxControls
    {
        [ComVisible(true)]
        [ClassInterface(ClassInterfaceType.None)]
        [Guid("1FC0D50A-4803-4f97-94FB-2F41717F558D")]
        [ProgId("AxControls.HelloWorld")]
        [ComDefaultInterface(typeof(IHelloWorld))]
        public class HelloWorld : UserControl, IHelloWorld, IObjectSafety
        {
            #region IHelloWorld Members
    
            public string GetText()
            {
                return "Hello ActiveX World!";
            }
    
            #endregion
    
            #region IObjectSafety Members
    
            public enum ObjectSafetyOptions
            {
                INTERFACESAFE_FOR_UNTRUSTED_CALLER = 0x00000001,
                INTERFACESAFE_FOR_UNTRUSTED_DATA = 0x00000002,
                INTERFACE_USES_DISPEX = 0x00000004,
                INTERFACE_USES_SECURITY_MANAGER = 0x00000008
            };
    
            public int GetInterfaceSafetyOptions(ref Guid riid, out int pdwSupportedOptions, out int pdwEnabledOptions)
            {
                ObjectSafetyOptions m_options = ObjectSafetyOptions.INTERFACESAFE_FOR_UNTRUSTED_CALLER | ObjectSafetyOptions.INTERFACESAFE_FOR_UNTRUSTED_DATA;
                pdwSupportedOptions = (int) m_options;
                pdwEnabledOptions = (int) m_options;
                return 0;
            }
    
            public int SetInterfaceSafetyOptions(ref Guid riid, int dwOptionSetMask, int dwEnabledOptions)
            {
                return 0;
            }
    
            #endregion
        }
    }
    

[ComImport()] IObjectSafety is a native interface so we have to redefine it for managed .Net use. This is done with the ComImport() attribute, see ComImportAttribute Class on MSDN.
[Guid("CB5BDC81-93C1-11CF-8F20-00805F2CD064")] This is the GUID of the original IObjectSafety interface. Do not change it.
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] sets the COM interface type to Unknown, see InterfaceTypeAttribute Class on MSDN.

This is just a simple implemetation of the IObjectSafety interface that will mark the control as safe. In "real life" there would probably be some sort of logic to determine if the control is safe or not.

6. Create a .msi installer for the control

Before an ActiveX control can be used it must be installed and registered on the client. This can be done in a number of ways, from manually editing the registry to using regasm.exe, but we're going to create a Vistual Studio setup project to handle the installation for us.

  1. Right click the Visual Studio solution, select Add -> New Project and select Setup Project under Other Project Types.
  2. Call the project 'AxControlsInstaller' and click OK.
  3. Right click the 'AxControlsInstaller' project, select Add -> Project Output, select 'Primary output' from the 'AxControls' project and click OK.
  4. Right click 'Primary output from AxControls (Active)' and select Properties.
  5. Change the Register property from 'vsdrpDoNotRegister' to 'vsdrpCOM'.
  6. Right click the 'AxControlsInstaller' project and select Build.

The installer should now be located in the AxControlsInstaller's output folder (bin\Debug or bin\Release). In the corporate domain this .msi file can de run manually on the client, or automatically with a Group Policy.

7. Package the installer in a .cab file for web deployment

For public web sites we obviously can't deploy our ActiveX control to the client with a Group Policy. In this case we're gonna have to use Internet Explores built-in ability to download and install controls that are packaged in .cab files.

  1. Download the Microsoft Cabinet Software Development Kit.
  2. Unpack the kit to a local folder and copy Cabarc.exe to the 'AxControlsInstaller' folder.
  3. Create a new file named 'AxControls.inf' in the 'AxControlsInstaller' folder and add the following content:
    [version]
    signature="$CHICAGO$"
    AdvancedINF=2.0
    
    [Add.Code]
    AxControlsInstaller.msi=AxControlsInstaller.msi
    
    [AxControlsInstaller.msi]
    file-win32-x86=thiscab
    clsid={1FC0D50A-4803-4f97-94FB-2F41717F558D}
    FileVersion=1,0,0,0
    
    [Setup Hooks]
    RunSetup=RunSetup
    
    [RunSetup]
    run="""msiexec.exe""" /i """%EXTRACT_DIR%\AxControlsInstaller.msi""" /qn
    
  4. Click the AxControlsInstaller project and then click the Properties window (View -> Properties Window if it's not visible).
  5. Click the '...' button next to the PostBuildEvent property and add the following content:
    "$(ProjectDir)\CABARC.EXE" N "$(ProjectDir)AxControls.cab" "$(ProjectDir)AxControls.inf" "$(ProjectDir)$(Configuration)\AxControlsInstaller.msi"
    
  6. Right click the 'AxControlsInstaller' project and select Build.
  7. There should now be a 'AxControls.cab' file in the 'AxControlsInstaller' folder.

NB! Make sure you use ANSI encoding for the 'AxControls.inf' file or you will be unable to install the control.

8. Initialize and test the control with JavaScript

  1. Right click the AxControls solution, select Add -> New Project and select 'ASP.Net Web Application' under 'Web'.
  2. Call the project 'WebAppTest' and click OK.
  3. Right click the 'WebAppTest' project, select Add -> New Item and select 'HTML Page'.
  4. Call it 'index.html' and click OK.
  5. Add the following content to index.html:
    <html>
        <head>
    
            <object name="axHello" style='display:none' id='axHello' classid='CLSID:1FC0D50A-4803-4f97-94FB-2F41717F558D' codebase='AxControls.cab#version=1,0,0,0'></object>
    
          <script language="javascript">
    
            <!-- Load the ActiveX object  -->
            var x = new ActiveXObject("AxControls.HelloWorld");
    
            <!-- Display the String in a messagebox -->
            alert(x.GetText());
    
          </script>
        </head>
        <body>
        </body>
    </html>
    

    Note that 'classid' matches the GUID of the HelloWorld control.

  6. Right click 'index.html' and select 'Set as start page'.
  7. Right click the 'WebAppTest' project and select 'Set as startup project'.
  8. Copy 'AxControls.cab' from the 'AxControlsInstaller' folder to the same folder as index.html.
  9. Uninstall the control from the client by going to Control Panel -> Programs and Features, selecting 'AxControlsInstaller' on the list and clicking Uninstall. This forces Internet Explorer to download and install the .cab file and is an important step in case you've already installed the control.
  10. Run the application (F5). This will open 'index.html' in Internet Explorer.
  11. Internet Explorer will display a security warning, asking if you want to install 'AxControls.cab'. Click Install.
  12. When the page loads it should display a message box with the string you defined in HelloWorld's GetText() method.

If the message box displayed without any more warnings or errors we've implemented everyting correctly.

UPDATE

I forgot to write that you have to register the assembly containing the ActiveX control for COM interop. Right-click the project, select Properties, go to Build and check the "Register for COM interop" checkbox. This should solve the error some of you are seeing about "Automation Server can’t create this object" and similar error messages. You might need to add the [ComVisible(true)] attribute to the methods and properties you are exposing as well, but I haven't had time to test this.

4Mar/10

Error when editing People Search Core Results Web Part XSL

Today I was tasked with removing the "Add to My Colleagues" link from the people search results. I figured this would be easy, just edit the XSL of the People Search Core Results Web Part. I simply commented out the part that added the link:

<xsl:template name="DisplayAddToMyColleaguesLink">
   	<xsl:param name="url"/>

<!-- Remove "Add to My Colleagues" link

    <a href="{$url}">
		<xsl:value-of select="$AddToMyColleaguesText"/>
    </a><br/>

-->

</xsl:template>

I then went on to save the changes but much to my surprise (and horror) the Web Part displayed the following error:

System.ArgumentException: An item with the same key has already been added.
at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)...

Of course I assumed I had somehow made the XSL invalid with my editing, so I pasted in the original and clicked Save again, but the error would not go away. Unable to resolve the issue I ended up discarding the checkout and starting all over again. This time I did a simple Select All -> Copy -> Delete -> Paste, but even without making any modifications to the XSL I got the same error.

After some googling I found out that this is a known bug, and the workaround is to simply check in and publish the page. I did so, the error went away, and the "Add to My Colleagues" link was now gone from the search results.

4Mar/10

Error when uploading large files to SharePoint running on Windows Server 2008

A client was having problems uploading a 29 MB file to their SharePoint server. Internet Explorer would simply show the "This page can not be displayed" error page, so I figured the maximum upload size was probably set too low.  There's two ways to set this limit; through SharePoint Central Administration or with STSADM.

Using SharePoint Central Administration

  1. Click Start -> All Programs -> Administrative Tools -> SharePoint Central Administration
  2. Click Application Management -> Web application general settings
  3. On the Web Application General Settings page, click the Web application that you want to change.
  4. On Maximum upload size, type the maximum file size in megabytes that you want, and then click OK.

You can specify a maximum file size of 2047 MB.

Using STSADM

To set the max upload size to 100 MB use the following command:

  1. Click Start -> Run
  2. Type 'cmd' and click OK.
  3. Type 'stsadm -o setproperty -pn max-file-post-size -pv 100 -url http://server' and hit Enter.

However in the client's case the maximum upload size was already set to 100 MB, so clearly this was not the cause of the error. I thought maybe IIS was timing out, but realized that this couldn't be the problem either since the error happened the second the user clicked the Upload button and not 120 seconds later as would be expected with a time out. The default timeout value is 120 seconds, and I confirmed that this had not been changed.

It turns out this is an issue related to Windows Server 2008 when uploading files bigger than 28 MB as described in the bottom of KB925083. The solution is to add the following snippet of XML to the web application's web.config file after the <configSections> tag.

<system.webServer>
  <security>
    <requestFiltering>
      <requestLimits maxAllowedContentLength="104857600"/>
    </requestFiltering>
  </security>
</system.webServer>

Replace the actual value with the max upload size for you web application in bytes (1 Megabyte = 1048576 Bytes ).

2Mar/10

Small Basic – Because it’s never to early to start programming!

I don't remember exactly how old I was when I first started programming, but it was somewhere in early primary school. My dad had just bought us a used Commodore 64 and I started playing around with the Commodore Basic programming language. Those of you who remember BASIC probably also wrote something like this at one point:

10 PRINT OLAV IS COOL!
20 GOTO 10
RUN

Yeah it was fun, but not terribly useful, and doing anything graphical was a nightmare. However it was enough to spur my interest in programming in general, and after a few years on an IBM PS/2 8086 with QuickBASIC I eventually graduated to Visual Basic 4.5 with WinForms and the rest is history. Had it not been for these early years I might never have become a developer in the first place.

I don't have any kids, but I imagine there's alot of fellow developers out there who do, and I'm sure some of you might already have thought about what your kids will grow up to be one day. I'm not really a fan of parents telling their kids what career to choose, but as Homer Simpson once said "It never hurts to grease the engine a little!".

So if you want your kid to get the taste for programming why not introduce them to Small Basic? It's educational and might be the beginning of a long and prosperous life in the "wonderful world of software development".