Introduction
Have you ever wanted to break up that monolithic web project into multiple projects, or reuse those user controls and web forms in multiple web projects?
Currently, reusing web forms and user controls in multiple ASP.NET projects requires copying the associated aspx and ascx pages. You can put web controls in separate assemblies, but you lose the design-time drag and drop that makes user controls and web forms so easy to create in the first place. If you've ever tried to put a user control or web form in a separate assembly, you probably ended up with a blank page or runtime errors. The reason is because LoadControl() actually reads the ascx or aspx file to populate the Controls collection and then binds them to your class variables. If the ascx file is not found, no controls are added unless you have done so in your code (as is done with WebControls).
What I wanted was the ability to dynamically call LoadControl() from another assembly to reuse user controls in multiple web projects without copying a bunch of ascx files around. Too much to ask?
Would it be possible to embed those ascx and aspx files as assembly resources and then load them? LoadControl() expects a virtual path, and there did not appear to be any way to load a control from a resource stream. Then I found this:
The Solution
The Virtual Path Provider in ASP.NET 2.0 can be used to load ascx files from a location of your choosing. For my purposes I've decided to store the ascx files in the assembly itself. No more outdated ascx pages that don't work with the updated assembly. Only one file to deploy, the assembly itself, and if you add the assembly as a reference, VS will copy it automatically! To do embed the ascx/aspx file into the assembly, you must change the Build Action of the file on the property page to Embedded Resource, the virtual path provider we create will do the rest.
When a Virtual Path needs to be resolved, ASP.NET will ask the most recently registered Virtual Path Provider if the file exists, and if it does, it will call GetFile to obtain the VirtualFile instance.
Before we can load a resource, we need to know what assembly the resource is located in and the name of the resource to load. I've chosen to encode this information into the virtual path. My final URL looks like this:
~/App_Resources/WebApplicationControls.dll/WebApplicationControls.WebUserControl1.ascx
It's a bit lengthy, but it includes all the information I need. I don't want to intercept all URLs, so we need to be able to identify which URLs to process and which ones to let the default virtual path provider handle. To do this, I've chosen to process only URLs located in App_Resources. This folder doesn't exist, and that's the point as all paths at this location will be intercepted. The second part contains the assembly name, and the final part is the resource name, which includes the namespace.
I've implemented the Virtual Provider as follows:
public class AssemblyResourceProvider : System.Web.Hosting.VirtualPathProvider
{
public AssemblyResourceProvider() {}
private bool IsAppResourcePath(string virtualPath)
{
String checkPath = VirtualPathUtility.ToAppRelative(virtualPath);
return checkPath.StartsWith("~/App_Resource/",
StringComparison.InvariantCultureIgnoreCase);
}
public override bool FileExists(string virtualPath)
{
return (IsAppResourcePath(virtualPath) ||
base.FileExists(virtualPath));
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsAppResourcePath(virtualPath))
return new AssemblyResourceVirtualFile(virtualPath);
else
return base.GetFile(virtualPath);
}
public override System.Web.Caching.CacheDependency
GetCacheDependency( string virtualPath,
System.Collections.IEnumerable virtualPathDependencies,
DateTime utcStart)
{
if (IsAppResourcePath(virtualPath))
return null;
else
return base.GetCacheDependency(virtualPath,
virtualPathDependencies, utcStart);
}
}
IsAppResource is a private helper method used to determine if we should process the request or let the default provider process the request. Virtual Path Providers are chained together, so it is important that you call the base class. It was also necessary to override GetCacheDependency to return null, otherwise ASP.NET will try to monitor the file for changes and raise a FileNotFound exception. Notice that GetFile returns an instance of AssemblyResourceVirtualFile, this class provides an Open() method to get the resource stream, and is implemented as follows:
class AssemblyResourceVirtualFile : VirtualFile
{
string path;
public AssemblyResourceVirtualFile(string virtualPath) : base(virtualPath)
{
path = VirtualPathUtility.ToAppRelative(virtualPath);
}
public override System.IO.Stream Open()
{
string[] parts = path.Split('/');
string assemblyName = parts[2];
string resourceName = parts[3];
assemblyName = Path.Combine(HttpRuntime.BinDirectory,
assemblyName);
System.Reflection.Assembly assembly =
System.Reflection.Assembly.LoadFile(assemblyName);
if (assembly != null)
{
return assembly.GetManifestResourceStream(resourceName);
}
return null;
}
}
All we need to do here is parse virtualPath to get the assembly name and resource name. I'm assuming the assembly is located in the bin directory as it is the typical place for them, and GetManifestResourceStream does all the hard work.
All we need to do now is tell ASP.NET to use our virtual path provider. This can be done in one of two places: Global.asax Application_Start(), or a static method with the following signature in any class file in the App_Code directory:
public static void AppInitialize() { ... }
The important thing is that it must be registered before any virtual paths trapped by our provider can be resolved. I chose to put this in the Application_Start, which looks like the following.
protected void Application_Start(object sender, EventArgs e)
{
System.Web.Hosting.HostingEnvironment.RegisterVirtualPathProvider(
new AssemblyResourceProvider());
}
Now, we can load our custom controls into a PlaceHolder using the usual LoadControl, as follows:
protected void Page_Load(object sender, EventArgs e)
{
Control ctrl = LoadControl("/App_Resource/WebApplicationControls.dll/"+
"WebApplicationControls.WebUserControl1.ascx");
PlaceHolder1.Controls.Add(ctrl);
}
The URLs are a bit lengthy because they include information about the assembly and class name, but you can intercept any URL you want. I chose to prefix it with App_Resources to more easily identify what URLs to intercept. An alternate approach might be to define a Virtual Path Provider that uses reflection to search all assemblies in the bin directory for resources ending in ascx/aspx and intercept only those URLs with or without a namespace.
Using the Code
The demo project uses the Web application project update for VS 2005 which can be found here: MSDN.
| You must Sign In to use this message board. |
|
|
 |
|
 |
thanks a lot, but i try to integrate that and that work perfectly, but i receive a error when i add javascript function
i don't know where i can add my <script> or my .js file ??? i try in my webcontrol or in the master page where it call the loadcontrol. Nothing, always the same error. I try to put my js file in Embedded Resources ... nothing too.
CS0117: 'ASP.bin_securepaiementwebpanel_dll_securepaiementwebpanel_securepaiementweb_ascx' ne contient pas de définition pour 'CallPinpad'
à System.Web.Compilation.AssemblyBuilder.Compile() à System.Web.Compilation.BuildProvidersCompiler.PerformBuild() à System.Web.Compilation.BuildManager.CompileWebFile(VirtualPath virtualPath) à System.Web.Compilation.BuildManager.GetVPathBuildResultInternal(VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile) à System.Web.Compilation.BuildManager.GetVPathBuildResultWithNoAssert(HttpContext context, VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile) à System.Web.Compilation.BuildManager.GetVPathBuildResult(HttpContext context, VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile) à System.Web.UI.TemplateControl.LoadControl(VirtualPath virtualPath) à System.Web.UI.TemplateControl.LoadControl(String virtualPath) à SiSWeb.aaaaaatest.Button4_Click(Object sender, EventArgs e) dans C:\programmation2\Programmation\Softicket Develop\SISWeb\aaaaaatest.aspx.vb:ligne 21 à System.Web.UI.WebControls.Button.OnClick(EventArgs e) à System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument) à System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument) à System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument) à System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData) à System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I have a httphandler that deals with my page requests, when i try and view a page which is an embedded resource is get a blank page, i have noticed that your AssemblyResourceProvider is not fired when i try and handle the request. my question is do i need to some how register my handler to call the AssemblyResourceProvider? and if so how do i do that
Thanks in advance
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Don't worry mate got it working! IHttpHandler virtualPage = (Page)System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(adminPagePath, typeof(Page)); virtualPage.ProcessRequest(context);
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi,
I have an application where I am taking some input from the aspx page of the website and accordingly invoking the assembly to load the user control of that type.This is successfully achieved using VPP. But I also need to pass some parameters to the user control from the website and then load it. Is that possible using VirtualPathProvider? If not, then is there any other way to achieve it? Any help is appreciated.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
The virtual path provider just reads the file streams, the path provider doesn't actually create the control. ASP.NET still creates the instance of your control via LoadControl, which unfortunately doesn't allow injecting any constructor parameters, so you'll have to pass in your parameters after the control has been created.
|
| Sign In·View Thread·PermaLink | 4.00/5 |
|
|
|
 |
|
 |
I can't get this thing to work when running medium trust. I get ghe following error:
Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
[FileLoadException: Could not load file or assembly 'LibraryName, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. Access is denied.]
Is there any way to get it to work running medium trust?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Hi Kurt,
I tried to deploy a similar code on Sharepoint and encountered some issues. Firstly, what is the correct form of the overriden functions in the VirtualPathProvider: should they call the corresponding methods of the base object or Previous object methods as in a chain. See the link:
http://support.microsoft.com/kb/910441/en-us?spid=8940&sid=global[^]
My problem is that I am trying to load an embedded user control in a Sharepoint webpart and I get a Parser Error: "Could not load type ...". After debug I saw that AssemblyResourceVirtualFile.Open finds the ascx resource and returns it correctly but the error is thrown in the compilation process, before the control gets back from the Page.LoadControl() line. (type of project is Web Application Project) Do you have any idea on that?
Thank you, Victor
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
The code does call the base class when it does not handle the request, of course the idea is to replace the existing functionality so it does not call the base class when request is handled.
I have not developed controls for sharepoint so I am not familiar with the issues with sharepoint deployment. The link you provided shows the an VirtualPathProvider implementation for sharepoint, why not use that one?
What I can tell you is that there is a significant difference between how ASP.NET handles CodeBehind and CodeFile. When CodeFile is used the virtual path provider is called twice once for the aspx file and once for the cs/vb file. When I moved to VS 2008 and tried using CodeFile instead of CodeBehind I had some issues with the buildmanager complaining about path not matching or something. I don't remember exactly what the issue was, but I found it worked fine if I placed the code in the aspx file inline (<script runat="server"> block) instead, so you might give that a try.
Kurt
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
This works fine for me in development mode (VS 2005, localhost).
However, when running in production on the actual server, the resource can't be found.
I get a "404" error.
Anyone run into this problem?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I have a small bug in Page_Load above that you will see when deployed to virtual directory rather than root. LoadControl("/App_Resource... should be LoadControl("~/App_Resource"... note the '~'.
- Kurt
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Yeah, I saw that one. I'm actually trying to get an embedded page (aspx) to work.
I'm still playing around with some ideas, but at this point, I'm a little stumped.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I keep ending up with an error like the following:
The VirtualPathProvider returned a VirtualFile object with VirtualPath set to ' /sandbox/App_Resource/Resources.dll/Resources.WebForm1.aspx' instead of the expected '//sandbox/App_Resource/Resources.dll/Resources.WebForm1.aspx'. This is what I get when running in production, but things work fine in development mode (within VS 2005).
I also created a new class that contains the resources (embedded) and the provider class.
Anyone have a thought on this?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
The virtualFile parameter passed to the VirtualFile base class constructor must have the same value that was passed to you in GetFile.
- Kurt
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
It is...The code i'm using is the same posted; I only changed two things: - I moved the resource items into an actual Class Library called Resources.dll - I changed the paths to reflect the new namespace and assembly name.
As indicated, it works fine in dev mode.
I think the problem lies in the fact that the production application is running in a "Virtual Directory" (called "sandbox" in this case) under the main web site.
e.g.: \\testserver\mainsite.com\sandbox\default.aspx
When the provider tries to resolve the virtual path, it is looking at the "sandbox" name thinking it is the server/root source and getting confused. In the examples posted, everything runs at the root level, including dev mode.
I see you're in Denver. Gearing up for the DNC? I work downtown as well...Should be an interesting week commute-wise.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I am currently using the VirtualPathProvider in VS 2008 web site, but I am not mapping urls quite the same way described here. I downloaded the solution again to give it a try and sure enough for some reason this solution no longer works under IIS. Comparing with my existing solution I found the problem to be in having the dll in the path. There must have been some security update that causes IIS to block requests that look like /xxxx.dll/. A simple fix is to modify the Open method to append the .dll to assembly name and remove it from the links.
-- I have the option of working from home so I'll think I will be avoiding downtown the week of DNC. It's always nice to find local people to network with. Send me an email sometime perhaps we can have lunch. kurtharriger@comcast.net
-- Kurt Harriger
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Is this problem solved? Please advice. I get the same error. Localhost works great, at the server 404 error. Thanks and regards,
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Found my answer. Better answers still appreciated. The problem occurs with precompiled web sites.
There is a workaround. See http://sunali.com/2008/01/09/virtualpathprovider-in-precompiled-web-sites/
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
You might consider converting your Web Site Project to Web Application Project. The VPP does work in a Web Application Project when registered in App_Start().
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
 |
I tried your code... The assembly.GetManifestResourceStream(resourceName) of AssemblyResourceVirtualFile is returning null...How can I fixed this or what went wrong? I put this on other site also.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
You need to change the Build Action of your control in the properties window to Embedded Resource.
- Kurt
|
| Sign In·View Thread·PermaLink | 5.00/5 |
|
|
|
 |
|
 |
I'm having a similar issue.
I have my page.aspx set as "Embedded Resource" and my page.aspx.vb file set as "Embedded Resource". In this fashion, the provider can't find the page.aspx.vb file.
If I set the page.aspx.vb file to "Compile", then the page.aspx.vb file can't find the controls on the page.aspx (design time) and the assembly can't/won't compile.
The only way I can get this to work is if I remove the code behind file and put all logic in the main .aspx page using <script runat="server">. </script>
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
I had the same issue.
If I create a project of Class Library project and embed a .ascx page in it, the site makes a call to both the .ascx and .ascx.cs files and fails on the 2nd call. However, if you create a Web Application project and compile that into a DLL, then this works fine (the framework only makes a single call to the .ascx file in this case). I suspect there is a way to get this to work in a Class Library project, but I haven't had time to dig any further into that. However, move your stuff into a Web Application project and it should work.
BTW, excellent article Kurt - much appreciated.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
Something like this:
Error 1 File 'App_Resource/WebApplicationControls.dll/WebApplicationControls.WebForm1.aspx' was not found. C:\Samples\Web\ASP.Net\WebApplicationTest\WebApplication1\Default.aspx 18 72 WebApplication1
It seems that VS2005 isn't able to find the file (of course it is not using the written VPP, so how could it find the ascx/aspx)?
Is there a possibility to turn off this specific error? It is really disturbing, if you'ev got a page using a lot of referenced embedded controls/pages.
Otherwise it works great as far, but i still have to try compatbility with master pages and using it with Webparts (the embedded controls in my case will be derived from Webparts).
Tnx a lot!
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
 |
What if you have user controls referencing, let's say, an image that is also an embedded resource? Let's say your structure is like this:
SharedControl.ascx /Images/anImage.jpg
Inside of your control, you would want to reference the image by using a path like: Images/anImage.jpg. This works fine in there (especially if you browse to a page that references the SharedControl.ascx), but once you reference the shared control from another project (let's say a website), the image is NOT displayed. That's because the path to the embedded resource is not correct. Is there a good way of handling this?
Matt - please respond to this address: codprojectembeddedresources.20.mattyoungblut@spamgourmet.com
|
| Sign In·View Thread·PermaLink | 2.00/5 |
|
|
|
 |
|
|