Saturday, May 13, 2006
« Extending controls to handle Nullable ty... | Main | CSLA .NET updates »

I’ve run into a spot where I’m stuck, and I’m hoping someone has an idea.

 

CSLA .NET 2.0 includes an ASP.NET data source control: CslaDataSource. This works well, except for one issue, which is that it doesn’t refresh the metadata for your business objects unless you close VS 2005 and reopen the IDE.

 

The reason for this problem is that your business assembly gets loaded into memory so the data source control can reflect against it to gather the metadata. That part works fine, but once an assembly is loaded into an AppDomain it can’t be unloaded. It is possible to unload an entire AppDomain however, and so that’s the obvious solution: load the business assembly into a temporary AppDomain.

 

So this is what I’m trying to do, and where I’m stuck. You see VS 2005 has a very complex way of loading assemblies into ASP.NET web projects. It actually appears to use the ASP.NET temporary file scheme to shadow the assemblies as it loads them. Each time you rebuild your solution (or a dependant assembly – like your business assembly), a new shadow directory is created.

 

The CslaDataSource control is loaded into the AppDomain from the first shadow directory – and from what I can tell that AppDomain never unloads, so the control is always running from that first shadow directory. And then – even if I use a temporary AppDomain – the business assembly is also loaded from that same shadow directory, even if newer ones exist.

 

And that’s where I’m stuck. I have no idea how to find out the current shadow directory, and even if I do odd things like hard-coding the directory, then I just get in trouble because the new AppDomain thinks it has a different Csla.dll than the AppDomain hosting the Web Forms designer.

 

Here’s the code that loads the Type object within the temporary AppDomain:

 

    public IDataSourceFieldSchema[] GetFields()

    {

      List<ObjectFieldInfo> result =

        new List<ObjectFieldInfo>();

 

      System.Security.NamedPermissionSet fulltrust =

        new System.Security.NamedPermissionSet("FullTrust");

      AppDomain tempDomain = AppDomain.CreateDomain(

        "__temp",

        AppDomain.CurrentDomain.Evidence,

        AppDomain.CurrentDomain.SetupInformation,

        fulltrust,

        new System.Security.Policy.StrongName[] { });

      try

      {

        // load the TypeLoader object in the temp AppDomain

        Assembly thisAssembly = Assembly.GetExecutingAssembly();

        int id = AppDomain.CurrentDomain.Id;

        TypeLoader loader =

          (TypeLoader)tempDomain.CreateInstanceFromAndUnwrap(

            thisAssembly.CodeBase, typeof(TypeLoader).FullName);

        // load the business type in the temp AppDomain

        Type t = loader.GetType(

          _typeAssemblyName, _typeName);

       

        // load the metadata from the Type object

        if (typeof(IEnumerable).IsAssignableFrom(t))

        {

          // this is a list so get the item type

          t = Utilities.GetChildItemType(t);

        }

        PropertyDescriptorCollection props =

          TypeDescriptor.GetProperties(t);

        foreach (PropertyDescriptor item in props)

          if (item.IsBrowsable)

            result.Add(new ObjectFieldInfo(item));

      }

      finally

      {

        AppDomain.Unload(tempDomain);

      }

      return result.ToArray();

    }

 

This replaces the method of the same name from ObjectViewSchema in the book.

 

Notice that it creates a new AppDomain and then invokes a TypeLoader class inside that AppDomain to create the Type object for the business class. The TypeLoader is a new class in Csla.dll that looks like this:

 

using System;

using System.Collections.Generic;

using System.Text;

 

namespace Csla.Web.Design

{

  /// <summary>

  /// Loads a Type object into the AppDomain.

  /// </summary>

  public class TypeLoader : MarshalByRefObject

  {

    /// <summary>

    /// Returns a <see cref="Type">Type</see> object based on the

    /// assembly and type information provided.

    /// </summary>

    /// <param name="assemblyName">(Optional) Assembly name containing the type.</param>

    /// <param name="typeName">Full type name of the class.</param>

    /// <remarks></remarks>

    public Type GetType(

      string assemblyName, string typeName)

    {

      int id = AppDomain.CurrentDomain.Id;

      return Csla.Web.CslaDataSource.GetType(

        assemblyName, typeName);

    }

  }

}

 

Since this object is created in the temporary AppDomain, the business assembly is loaded into that AppDomain. The Type object is [Serializable] and so is serialized back to the main AppDomain so the data source control can get the metadata as needed.

 

This actually all works – except that it doesn’t pick up new shadow directories as they are created.

 

Any ideas on how to figure out the proper shadow directory are appreciated.

 

Honestly, I can’t figure out how this works in general – because obviously some part of the VS designer picks up the new shadow directory and uses it – even though the designer apparently doesn’t. I am quite lost here.

 

To make matters worse, things operate entirely differently when a debugger is attached to VS than when not. When a debugger is attached to VS then nothing appears to pick up the new shadow directories – so I assume the debugger is interfering somehow. But it makes tracking down the issues really hard.


Saturday, May 13, 2006 3:37:29 PM (Central Standard Time, UTC-06:00)
You can't fix this, it's a vs.net thing. Guthrie confirmed this to me when I asked him about exactly the same thing, as I had the same probs with my llblgenprodatasource controls.
Sunday, May 14, 2006 12:43:17 AM (Central Standard Time, UTC-06:00)
What's wrong with

if (DesignMode)
{
Assembly thisAssembly = Assembly.GetExecutingAssembly() ;
writer.Write(thisAssembly.CodeBase) ;
}

... in your DataSource?

In my case it returns f:///C:/Win2k3/Microsoft.NET/Framework/v2.0.60727/Temporary ASP.NET Files/website1/5e355cdb/_shadow/a5c9cb63/bin/17/SomeControl.dll
Sunday, May 14, 2006 12:44:03 AM (Central Standard Time, UTC-06:00)
PS: The above works in Design Mode .. i.e. in VS.NET 2005.
Sunday, May 14, 2006 12:49:13 AM (Central Standard Time, UTC-06:00)
Geez, it would help if I wrote my message in one go .. but then it wouldn't be as much fun.

So yes the above approach also picks up changes - automatically. No "reopening" of the aspx or restarting the IDE required.

Maybe I completely misunderstood your problem :-/
Monday, May 15, 2006 5:20:15 AM (Central Standard Time, UTC-06:00)
I know it's not really a fix, but is this something that would be solved using the newly released Web Application Projects?
anonymous
Monday, May 15, 2006 9:58:25 AM (Central Standard Time, UTC-06:00)
Sahil, I thought I tried that - originally I was doing an assembly LoadFrom() using the codebase from the current assembly - but that codebase wasn't changing to the new shadow directories as I rebuilt my business DLL. However, it occurs to me that I was testing through the debugger when I did that - and the debugger appears to interfer with VS changing the appdomain, etc. I'll try that again without using the debugger and see if it works.
Monday, May 15, 2006 10:44:57 PM (Central Standard Time, UTC-06:00)
Unfortunately that doesn't work. In fact it is the idea I did try first, and it simply doesn't work, because once the CslaDataSource control is loaded into VS 2005 that AppDomain (and thus that particular shadow directory) is used forever.

I do think I have a solution though (but it is a bit of a hack). The shadow directory names follow a very consistent pattern, and given one you can find the others - then use a DirectoryInfo object to determine the newest shadow directory and load the business assembly from that directory. Ugly, but I have it (mostly) working...
Wednesday, May 17, 2006 8:52:01 PM (Central Standard Time, UTC-06:00)
Perhaps the key difference is that my CslaDataSource control is _not_ in the solution along with the web project. I am doing a file reference (such as it is in the web) to Csla.dll, while I'm doing a project reference to my business assembly. In fact, I'm doing my testing in the ProjectTracker20cs solution.

In that setting, I am not finding any scenario under which the Csla.dll is ever loaded from anything but that very first AppDomain and shadow directory. The only way to get the IDE to use a different Csla.dll is to entirely close VS and reopen it.

Of course my goal isn't to get a new version of Csla.dll - but since CslaDataSource is always loaded from Csla.dll, it is thus always loaded from that very first shadow directory. That leaves me with no path to the current (most recent) shadow directory.

Someone emailed me suggesting the use of System.Web.Compilation.BuildManager - which seemed promising. But I couldn't get it to load any referenced assemblies at all - only auto-generated assemblies based on the App_Code directory.

Unfortunately, at the moment, I may have to roll with my DirectoryInfo hack. That does appear to work in my limited testing thus far.
Thursday, May 18, 2006 12:26:15 PM (Central Standard Time, UTC-06:00)
Rocky -

Yes the file based reference would make a difference. Did you try adding a .refresh file to the reference?

Sahil
Friday, May 19, 2006 9:29:03 AM (Central Standard Time, UTC-06:00)
I don't understand what you mean by ".refresh" file?
Friday, May 19, 2006 1:44:18 PM (Central Standard Time, UTC-06:00)
A .refresh file can be added to a file based reference, so whenever the file changes (filewatcher?), it is autocopied into the solution. So the file based reference then starts working like a project based reference. The .refresh file is simply an xml file, and this is something new in Vstudio 2k5.
Sunday, June 04, 2006 11:14:36 PM (Central Standard Time, UTC-06:00)
I was kinda waiting for a response from Rockford Lhotka to that latest comment :-)
dotnetjunkie
Friday, July 21, 2006 2:52:42 PM (Central Standard Time, UTC-06:00)
.refresh is not an xml file. it is a text file with one line pointing to the reletive path of the dll from another location:

..\Library\ThirdParty\TRObjects.dll

Use these sparingly. Using .refresh files can slow down your build time drastically.
Friday, July 21, 2006 3:08:06 PM (Central Standard Time, UTC-06:00)
The issue with CslaDataSource is quite different from what a .refresh file is supposed to solve. The issue with CslaDataSource is that there is no supported way to determine the current shadow directory from which VS 2005 loads assemblies that are referenced by a web project.

It isn't that VS 2005 itself doesn't load them or anything - it does that by creating a new shadow directory every time you rebuild the solution. The problem is that there is no supported way to get that information. Microsoft has a way - it is buried inside .NET, with some of the methods marked as internal.

In CSLA .NET 2.0.3 I do have a solution, and though it is a bit of a hack, it is quite clear that the ONLY answers are hacks - because the real solutions aren't supported or public in scope. You can find 2.0.3 at www.lhotka.net/cslanet/download.aspx, and you can find a description of my solution in the change log for that version.
Comments are closed.