So, there’s been somewhat of a commotion about this thing recently so I decided to give it a try. Let me mention several things first – I’m still wondering whether the sandbox solutions will be that popular and widely used to start with and whether the ability to create user controls for web parts is that important and effort-saving (well, it’s been a practice for me to use user controls with web parts but that’s not so for most of my colleagues), but despite this temperate pessimism of mine I decided to create a small POC that demonstrates how this can be implemented.
So let me start with several words about why the TemplateControl.LoadControl method is not working in a sandbox solution. The reason is quite simple – your code in a sandbox solution actually executes in a console application – the SPUCWorkerProcess.exe. So basically what you have is that a console application has created an HttpContext and a Page instance, in which your web part class is also instantiated – it is actually the only control (in some cases you can have a couple more) on that page. And in this mock-like asp.net environment you don’t have some crucial asp.net pieces as the HostingEnvironment and the VirtualPathProvider – without these two the LoadControl method simply won’t work (I may explain the sandbox and SharePoint user code intrinsic workings in another posting). And it is not that you don’t have access to the file system, even if you had, the LoadControl method still wouldn’t work because there wouldn’t be a VirtualPathProvider to provide the file to the asp.net compiler. The bad news is that the same holds for the TemplateControl.ParseControl method too.
And to the idea behind the implementation: I first saw a codeplex project - SharePoint Developer Tools for Visual Studio 2010 – it actually had a custom VS 2010 project template for a visual sandbox web part … but it didn’t work – it didn’t compile due to one error in this line:
Controls.Add(new ASP.SandboxedVisualWebPart1_ascx());
I immediately understood what the idea behind this was: a class name like this one - ASP.somecontrol_ascx - gives away a class generated by the asp.net runtime or by the aspnet_compiler.exe tool from an ascx file. So, aspx and ascx files get eventually compiled to real code Page and Control classes in temporary assemblies that the asp.net runtime creates if not existing and loads when you navigate the aspx page or use the ascx control. These assemblies can be created also using the aspnet_compiler.exe utility that comes with the .NET framework, which is used primarily to precompile your web sites when you deploy them. But you can also use this tool to compile any ascx (and aspx) file that you have – in our case the tool can be used to compile the user controls of the sandbox web parts. So, the tool will spit out one or several assemblies that contain the compiled classes of the ascx files but there are two issues with circular references:
- you cannot add a reference to this assembly in your web part assembly because the former itself has a reference to your assembly
- the second circular reference is one of class usage – you first need to have your assembly compiled before the ascx assemblies can be created (since they need the code behind classes from your assembly), but you wouldn’t be able to compile the web part assembly if it uses the ascx compiled classes (which could be created only after your assembly is compiled) – what a paradox.
And now, here’re the steps that I used to solve the above mentioned issues – note that all these are executed as a post build step of the web part project (the actual implementation is with a PowerShell script file):
- first step is to copy all ascx files that you have in the project to a temporary folder and use the aspnet_compiler.exe tool to create assembly (assemblies) containing the compiled ascx classes
- second step (this solves the first circular reference issue) is to use the ILMerge tool (it’s a free Microsoft tool that can be downloaded from here – note that it doesn’t come with the .NET installation) to merge the compiled ascx assembly (assemblies) with the web part assembly – this is what the ILMerge tool does – merging assemblies (great thing). And now you will have the ascx compiled classes right into your web part assembly.
- third step comes to the second circular reference issue – as I mentioned these steps are executed as a post build step which means that you have the web part assembly already compiled – but how did that happen if you need to reference the compiled ascx class. The answer is simple – you don’t reference it at all or at least not as a class – here is how this is possible: with a standard visual web part you use the user control via a member variable whose type is actually the code behind class of the user control and the only “reference” to the user control is the LoadControl call in the CreateChildControls method of the web part where the control is created and its instance gets cast to the code behind class member variable. And we have the same here, the only difference is the control instantiation in the CreateChildControls method and this is the key line of code for that:
_userCtrl = (UserControl1)Activator.CreateInstance(Type.GetType("ASP.usercontrol1_ascx"));
As you see – a little bit of reflection (will this pass the security checks in the sandbox - yes) and the ascx compiled class instance is created providing only its class name as a string (which is easily deducible from the name of the ascx file).
You can download the sample project which demonstrates this technique (or rather work-around) from here. And I want to make a big NOTE here that this is only a POC, the implementation is a bit sloppy and probably won’t work in all cases and/or in more complex scenarios. Also in the PowerShell script that implements the post-build step there’re several hard-coded values for file locations and names which may break the whole thing. So, having said that let me outline shortly the steps that you can use to create a simple sandbox solution that uses user controls in web parts:
- Open Visual Studio 2010 as administrator
- Create an Empty SharePoint Project (selecting the “Deploy as sandbox solution” option) – e.g. SandboxWebPart
- Add a new Web Part item to the project (e.g. WebPart1)
- Add a new User Control item to the project (e.g. UserControl1)
- Drag the newly create ascx item (UserControl1.ascx) from below the ControlTemplates/SandboxWebPart folder and drop it below the web part’s node – WebPart1
- Delete the ControlTemplates mapped folder (you won’t be able to build the package with it)
- This is optional – open the UserControl1.ascx.cs and UserControl1.ascx.designer.cs and change the namespace to be the namespace that you have in the web part’s code file. If you change the namespace in these two files you will have to change the Inherits attribute in the Control directive in the UserControl1.ascx file so that it reflects the new full name of the code-behind class.
- This is important – in the UserControl1.ascx in the Control directive at the top change the AutoEventWireup to false – otherwise you will receive security errors at runtime (sorry, you will need to hook your events explicitly).
- From the properties pane of the UserControl1.ascx – set the Deployment Type property to NoDeployment (remember you cannot deploy ascx files in a sandbox solution)
- Add some code to your web part so that it can use the user control – first create a member variable with the user control’s code-behind class type:
protected UserControl1 _userCtrl;
- Add this code to the web part’s CreateChildControls override method:
_userCtrl = (UserControl1)Activator.CreateInstance(Type.GetType("ASP.usercontrol1_ascx"));
this.Controls.Add(_userCtrl);
- Add the postbuild.ps1 file from the sample project into the project root folder
- Open the project properties page and in the Build Events section put this in the “Post-build event command line” text box:
PowerShell -command "set-executionpolicy -ExecutionPolicy bypass"
PowerShell -command "$(ProjectDir)postbuild.ps1" '$(SolutionDir)' '$(ProjectDir)' '$(TargetDir)' '$(TargetFileName)' '$(ConfigurationName)'
- Big NOTE – here – remember that you’re running Visual Studio as administrator and you’re first changing the execution policy setting of PowerShell and then you will run a PowerShell script which may be a huge security threat – make sure that you check carefully the code in the postbuild.ps1, so that you know what it does and how it does it. Another note – instead of using the script as a post-build step you can run it standalone, after you make a normal build of the project – in this case you will have to provide all command line arguments that it expects.
- Make sure that you have installed the ILMerge tool (see above) to this location - C:\Program Files (x86)\Microsoft\ILMerge (it is hard-coded in the ps script)
- Build the solution – in the output window you should see something like this:
------ Rebuild All started: Project: SandboxWebPart, Configuration: Debug Any CPU ------
SandboxWebPart -> c:\Projects\SandboxWebPart\SandboxWebPart\bin\Debug\SandboxWebPart.dll
asxc compilation started...
Compiling ascx files:
C:\Projects\SandboxWebPart\SandboxWebPart\ascxtmp\UserControl1.ascx
Utility to precompile an ASP.NET application
Copyright (C) Microsoft Corporation. All rights reserved.
Merging assemblies:
C:\aa618f13-0fa9-4dc8-b6ee-714f24d847f2\bin\App_Web_wagenclm.dll
C:\aa618f13-0fa9-4dc8-b6ee-714f24d847f2\bin\SandboxWebPart.dll
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========
- You can now run deploy and check your web part with a user control in it (hope this works for you).