Sunday, December 04, 2005

Bug: IE doesn't resolve URIs to behaviors correctly

URI's in stylesheets are resolved relative to the stylesheet, not relative to the page that loaded the stylesheet.

It's the only way that makes any sense. Take images for example:
/* this is somestyles.css */
body
{
background-image:url(images/background.gif)
}

If you resolved the path to background.gif from the page, rather than from somestyles.css then every page would have to sit at the same level as the images directory, which precludes having a site heirachy.

Fortunately this is not how the world works - URIs are resolved based on the base URI of the stylesheet, and everyone's happy...

...except IE (6, but presumably 5+) doesn't do this for DHTML behaviours - the path to the HTC is resolved based on the page that the behaviour applies to. WTF!?

That I never noticed this before is a testament to how little I've ever used DHTML behaviours (I regard them as some kind of voodoo), but that there's been no public outcry presumably means no-one else is either. Or did I miss some kind of wierd DOCTYPE setting to force IE into 'not crap' mode?

(Sure you can make the path root-relative, ie
.someClass
{
behavior:url(/behaviors/Moo.htc)
}
...but imagine if you're writing an app in a subdirectory (which is the normal VS.Net paradime):
.someClass
{
behavior:url(/MyApp/behaviors/Moo.htc)
}
'MyApp' might be the name you use in development, but it might be MyAppDaily for the dailybuild, and it might be just / (as in, run at the root of the site) when you eventually deploy. Want to keep changing your CSS per-environment? I don't think so)

References:
The documentation - not very explicit on how it's expected to work
Other people who have noticed: 'Keith' (comment 15), Dean Edwards, 'Lithium' on microsoft.public.scripting.jscript, but unless my Google-fu is failing there's not many of them.

log4net Context problems with ASP.Net thread agility

Even after my recent revalations about how ASP.Net's agile threading model precudes storing things in threadslots, ThreadStatic's or CallContext almost without exception[1], it still took me until just the other day to realize an important ramification of that:
log4net contexts are broken[2] under ASP.Net
A log4net context is a bag where you can put data that can be referenced by the log4net appenders when the logging calls are rendered. This supports the model where you just publish logging data that's relevant to your immediate scope, and the log4net renderer (based on the pattern you put in your config) just appends all the extra data that you probably want to log (like the CustomerID who's logged in or somesuch).

There are three log4net contexts: GlobalContext, ThreadContext and LogicalThreadContext, which do pretty much what you'd expect - provide places to hang contextual data for logging calls that's either global, thread-local or 'logical call' local (ie propagates across remoting calls). Unfortunately that's just not good enough in an ASP.Net environment, since we now know that neither CallContext nor Thread Local Storage slots get propagated onto the new thread when ASP.Net does a thread switch. Since that's where the ThreadContext and LogicalThreadContext are actually stored, ASP.Net's thread switch either blows away your context (in the case of LogicalThreadContext), or overwrites it (in the case of ThreadContext).

There's nothing fundamentally wrong with log4net, it's just you can't use log4net's ThreadContext or LogicalThreadContext within ASP.Net and expect to get a per-request logging context, though you'd be forgiven for imagining otherwise.

Demo: Storing the requested page URL in ThreadContext, so it's liable to be overwritten if a thread switch happens because two requests come in concurrently:
[3164] INFO  ASP.global_asax /ASPNetThreadingDemo/SlowPage.aspx - Begin request for /ASPNetThreadingDemo/SlowPage.aspx
[3164] INFO  ASP.global_asax /ASPNetThreadingDemo/SlowPage.aspx - End request for /ASPNetThreadingDemo/SlowPage.aspx
[1996] INFO  ASP.global_asax /ASPNetThreadingDemo/SlowPage.aspx - Begin request for /ASPNetThreadingDemo/SlowPage.aspx
[3164] INFO  ASP.global_asax /ASPNetThreadingDemo/FastPage.aspx - Begin request for /ASPNetThreadingDemo/FastPage.aspx
[1996] INFO  ASP.global_asax /ASPNetThreadingDemo/SlowPage.aspx - End request for /ASPNetThreadingDemo/SlowPage.aspx
[1996] INFO  ASP.global_asax /ASPNetThreadingDemo/SlowPage.aspx - End request for /ASPNetThreadingDemo/FastPage.aspx

See how the last logging call has 'SlowPage' in context, even though the request that's finishing is actually for 'FastPage' (I was expecting exactly the reverse, but hey).

It's pretty ironic really, since it was log4net I used to confirm how the threading really worked in the first place, but of all the things I looked at, log4net contexts wasn't one of them. Trouble is, if you're using log4net contexts in ASP.Net this is exactly the kind of thing you'd[3] be doing: setting things up in BeginRequest so they're there for all logging calls throughout the system.

The Workaround:
Fortunately log4net's so flexible that it already contains the solution. For all the contexts, log4net supports the concept of delayed resolution of the context value. To be more specific, log4net gets the context value by calling ToString() on the contents of the context bags when they're frozen and attached to a logging call. If rather than adding the actual value, you add an object who's ToString() provides the value, then you've deferred resolution of what the actual value was.

So in this case, rather adding the value of CustomerID directly into the ThreadContext, you'd add a CustomerIdProvider object instead:
Public Class CustomerIdProvider
 Public Overrides Function ToString() As String
     Return HttpContext.Current.Items("CustomerId")
 End Function
End Class
Since this now leverages HttpContext, you can put it in GlobalContext as well - it's automatically thread-safe.

It's a bit of a drag, but you're going to have to do this for all your non-global log4net context data[4]. It's pretty easy to generalise the class above into an all-purpose HttpContextValueProvider though, so you won't have to spawn a myriad of classes.

The Solution:


log4net's going to have to have an AdaptiveContext. This is a context bag that's stored either in CallContext or in HttpContext, depending on the environment that the application is running within. I'm already doing this for anything I previously put in CallContext in my business layer, but I haven't got round to grafting one onto log4net yet.


[Update: I posted a follow-up post about the need to also implement IFixingRequired - please read. Also, please note the date on this post is 2005, I don't claim this as all current information. YMMV]


[1] Ok, if you don't store anything in TLS / CallContext prior to Page.Init, then you'll be ok (given the current implementation anyway).
[2] ...depending on how you're using them
[3] I.E. 'me, until I realised all this'
[4] Despite all this, log4net's still the best logging framework I know of by far.

Wednesday, November 16, 2005

Get VS2005 for free!

(or 'What to do if the boss won't let you upgrade')

...if you've already got VS 2003 that is.

I know many developers sit and watch the VS2005 demos and thing how brillant it all is, and how it's a pain they'll not be using it for x months/years because their employer is so backwards / tight / sceptical. But there's a whole list of things I see in demos time and time again that can be done right now if you know how. Sure, you won't always get the IDE support, and it might not be as well rounded, but if you've only been using vanilla VS2003, then you really want to think about some of the following. Buy me a beer with the cash you save.

IDE
Unit Testing: Download NUnit and TestDriven.NET. Note how double-clicking the test failure in TestDriven.NET actually takes you to the failing line, unlike VS2005. Enjoy using NestedTestCase like you can't in Whitbey.

Refactoring and Code Snippets: Download ReSharper and wonder how you ever did without it (or CodeRush + Refactor! if you can't use ReSharper because you're a VB guy)

Nullable Types: Just use those Sql types in the SqlClient namespace, and you're 90% there.

Functional programming / dependency injection: The new generic collections support cute methods to filter the collection (FindAll), where you pass in the filter behaviour as a generic delegate (eg Predicate). Great - but don't confuse the behavior-injection bit with the generic bit. If you're writing custom collections today, you can write Visitor-esque methods that do the same thing with either an interface or a non-generic delegate. If you're code-gen'ing, so much the better.

Developing without IIS: Write your own webserver, by spinning up a SimpleWorkerRequest and pointing it to a directory full of ASPX files

DebuggerVisualisers: Open that mcee_cs.dat file in (vs)\Common7\Packages\Debugger and you'll know what to do

DebuggerProxies: Refactor the offending class. No excuses.

Strongly-typed config files: Use a XmlSerializerSectionHandler to serialize your .config blocks directly onto settings classes. Retrieve the settings class early (eg Application_OnStart) to get FailFast (sure it's not at compile-time, but...)

Tracepoints: Just use log4net and have that diagnostic stream available all the time, even when the debugger's not attached (you can update the logging configuration on the fly, see...)

Class Designer: Sparx System's EnterpriseArchitect is an excellent UML tool that's not expensive and does a pretty good job (especially compared to how pants Visio is)

Object Test Bench: Write your playing-with-the-class (spiking) code as a unit test. That way, when you've finished playing, you've also written a regression test for the behaviour you're about to use elsewhere.

ASP.Net

Master Pages: Either use one of the free reverse-implementations for ASP.Net 1, or use a HTML editing tool that does support templates, like DreamWeaver. While you're at it note that DreamWeaver still beats the pants of the HTML editor in Whitbey, even if the gap is closing.

In-HTML intellisense for anchors etc...: Use a half-decent HTML editor that's got broken link detection (see above)

ObjectBindingSource: It's a little known fact that any object that implements IComponent can already be used as a data binding source in ASP.Net, though it's fiddly to get it on the form unless it's actually Component. I think (total conjecture here) this is what CSLA does.

TwoWayDataBinding: There are existing TwoWayDataBinding implementations for ASP.Net 1 (here's another), that don't require you to subclass all your controls
like an eejit

Operator Overloading (& Continue, using blocks etc...): Use C# :-)

[phew. Did I miss any?]

Friday, November 04, 2005

Avoid public constants in .Net

I always advise people to avoid / be wary of public constants in .Net (internal / private is fine), and here's a good explanation of why:

http://haacked.com/archive/2005/01/02/1796.aspx

Why aren't bank notes perforated?

Never got the right change? Trouble breaking a note?

Why not just tear it in half?

$20 -> 2x $10
$10 -> 2x $5

Easy as.

When we had 'real' money (that is: coins whose value was equal to the amount of gold in the coin itself) you could do this. So what's wrong with perforating bank notes so we can do the same with them?

Wednesday, November 02, 2005

ThreadStatic, CallContext and HttpContext in ASP.Net

Summary:
Even if you think you know what you're doing, it is not safe to store anything in a ThreadStatic member, CallContext or Thread Local Storage within an ASP.Net application, if there is the possibilty that the value might be setup prior to Page_Load (eg in IHttpModule, or page constructor) but accessed during or after.

[Update: Aug 2008 In view of the fairly large number of people continuing to link to this post I feel the need to clarify that this thread-swapping behaviour happens at a very specific point in the page lifecycle and not whenever-it-feels-like-it. My wording after the Jef Newson quote was unfortunate. That aside, I've been immensely gratified (and flattered) by the number of times I've seen this post cited within design discussions around dealing appropriately with HttpContext. I'm glad people found it useful.]

There's a lot of confusion about using how to implement user-specific singletons in ASP.Net - that is to say global data that's only global to one user or request. This is not an uncommon requirement: publishing Transactions, security context or other 'global' data in one place, rather than pushing it through every method call as tramp data can make for a cleaner (and more readable) implementation. However its a great place to shoot yourself in the foot (or head) if you're not careful. I thought I knew what was going on, but I didn't.

The preferred option, storing your singletons in HttpContext.Current.Items, is simple and safe, but ties the singleton in question to being used within an ASP.Net application. If the singleton's down in your business objects, this isn't ideal. Even if you wrap the property-access in an if statement
if(HttpContext.Current!=null){
/* store in HttpContext */
}else{
/* store in CallContext or ThreadStatic */
}
... then you've still got to reference System.Web from that assembly, which tends to encorage more 'webby' objects in the wrong place.

The alternatives are to use a [ThreadStatic] static member, Thread local storage (which pretty much amounts to the same thing), or CallContext.

The problems with [ThreadStatic] are well documented, but to summarize:
Scott Hanselman gets it right, that ThreadStatic doesn't play well with ASP.Net, but doesn't fully explain why.

Storage in CallContext alleviates some of these problems, since the context dies off at the end of the request and GC will occur eventually (though you can still leak resources until the GC happens if you're storing Disposables). Additionally CallContext is how HttpContext gets stored, so it must be ok, right?. Irrespective, you'd think (as I did) that provided you cleaned up after yourself at the end of each request, everthing would be fine:
"If you initialize a ThreadStatic variable at the beginning of a request, and you properly dispose of the referenced object at the end of the request, I am going to go out on a limb and claim that nothing bad will happen. You're even cool between contexts in the same AppDomain

"Now, I could be wrong on this. The clr could potentially stop a managed thread mid-stream, serialize out its stack somewhere, give it a new stack, and let it start executing. I seriously doubt it. I suppose that it is conceivable that hyperthreading makes things difficult as well, but I also doubt that."
Jef Newsom

Update: This was the misleading bit. I do explain further later on that this thread-swap can only happen between the BeginRequest and the Page_Load, but Jef's quote creates a very powerful image I failed to immediately correct. My bad.

Trouble is that's exactly what happens. Trouble is that's almost what happens. Under load ASP.Net can migrate inbound requests from its IO thread pool to a queue taken up by it's worker process thread pool:
So at some point ASP.NET decides that there are too many I/O threads processing other requests. [...] It just takes the request and it queues it up in this internal queue object within the ASP.NET runtime. Then, after that's queued up, the I/O thread will ask for a worker thread, and then the I/O thread will be returned to its pool. [...] So ASP.NET will have that worker thread process the request. It will take it into the ASP.NET runtime, just as the I/O thread would have under low load.
Microsoft ASP.NET Threading Webcast
Now I always knew about this, but I assumed it happened early enough in the process that I didn't care. It appears however that I was wrong. We've been having a problem in our ASP.Net app where the user clicks one link just after clicking another, and our app blows up with a null reference exception for one of our singletons (I'm using CallContext not ThreadStatic for the singleton, but it turns out it doesn't matter).

I did a bit of research about how exactly ASP.Net's threading works, and got conflicting opinions-masquerading-as-fact (requests are thread-agile within a request vs requests are pinned to a thread for their lifetime) so I replicated my problem in a test application with a slow page (sleeps for a second) and a fast page. I click the link for the slow page and before the page comes back I click the link for the fast page. The results (a log4net dump of what's going on) surprised me.

What the output shows is that - for the second request - the BeginRequest events in the HttpModule pipeline and the page constructor fire on one thread, but the Page_Load fires on another. The second thread has had the HttpContext migrated from the first, but not the CallContext or the ThreadStatic's (NB: since HttpContext is itself stored in CallContext, this means ASP.Net is explicitly migrating the HttpContext across). Let's just spell this out again:
  • The thread switch occurs after the IHttpHandler has been created
  • After the page's field initializers and constructor run
  • After any BeginRequest, AuthenticateRequest, AquireSessionState type events that your Global.ASA / IHttpModules are using.
  • Only the HttpContext migrates to the new thread
This is a major PITA, because as far as I can see it mean the only persistence option for 'ThreadStatic'esque behavior in ASP.Net is to use HttpContext. So for your business objects, either you're stuck with the if(HttpContext.Current!=null) and the System.Web reference (yuck) or you've got to come up with some kind of provider model for your static persistence, which will need setting up prior to the point that any of these singletons are accessed. Double yuck.

Please someone say it ain't so.

Appendix: That log in full:
[3748] INFO  11:10:05,239 ASP.Global_asax.Application_BeginRequest() - BEGIN /ConcurrentRequestsDemo/SlowPage.aspx
[3748] INFO 11:10:05,239 ASP.Global_asax.Application_BeginRequest() - threadid=, threadhash=, threadhash(now)=97, calldata=
[3748] INFO 11:10:05,249 ASP.SlowPage_aspx..ctor() - threadid=3748, threadhash=(cctor)97, threadhash(now)=97, calldata=3748, logicalcalldata=3748
[3748] INFO 11:10:05,349 ASP.SlowPage_aspx.Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(now)=97, calldata=3748, logicalcalldata=3748
[3748] INFO 11:10:05,349 ASP.SlowPage_aspx.Page_Load() - Slow page sleeping....

[2720] INFO 11:10:05,669 ASP.Global_asax.Application_BeginRequest() - BEGIN /ConcurrentRequestsDemo/FastPage.aspx
[2720] INFO 11:10:05,679 ASP.Global_asax.Application_BeginRequest() - threadid=, threadhash=, threadhash(now)=1835, calldata=
[2720] INFO 11:10:05,679 ASP.FastPage_aspx..ctor() - threadid=2720, threadhash=(cctor)1835, threadhash(now)=1835, calldata=2720, logicalcalldata=2720, threadstatic=2720

[3748] INFO 11:10:06,350 ASP.SlowPage_aspx.Page_Load() - Slow page waking up....
[3748] INFO 11:10:06,350 ASP.SlowPage_aspx.Page_Load() - threadid=3748, threadhash=(cctor)97, threadhash(now)=97, calldata=3748, logicalcalldata=3748
[3748] INFO 11:10:06,350 ASP.Global_asax.Application_EndRequest() - threadid=3748, threadhash=97, threadhash(now)=97, calldata=3748
[3748] INFO 11:10:06,350 ASP.Global_asax.Application_EndRequest() - END /ConcurrentRequestsDemo/SlowPage.aspx

[4748] INFO 11:10:06,791 ASP.FastPage_aspx.Page_Load() - threadid=2720, threadhash=(cctor)1835, threadhash(now)=1703, calldata=, logicalcalldata=, threadstatic=
[4748] INFO 11:10:06,791 ASP.Global_asax.Application_EndRequest() - threadid=2720, threadhash=1835, threadhash(now)=1703, calldata=
[4748] INFO 11:10:06,791 ASP.Global_asax.Application_EndRequest() - END /ConcurrentRequestsDemo/FastPage.aspx


The key bit is what happens when FastPage's Page_Load fires. The ThreadID is 4748, but the threadID I stored in HttpContext in the ctor is 2720. The hash code for the logical thread is 1703, but the one I stored in the ctor is 1835. All data I stored in the CallContext is gone (even that marked ILogicalThreadAffinative), but HttpContext is still there. As you'd expect, my ThreadStatic is gone too.

Saturday, October 29, 2005

Type.GetType vs object.GetType() vs typeof()

On the aus-dotnet list someone asked how much slower Type.GetType(string) was compared to GetType() / typeof().

About 100 times slower, as it turns out:

http://www.stillhq.com/aus-dotnet/archives2/msg11648.html


What's interesting is the difference between object.GetType() and typeof() - not enough to worry about, but typeof() is definately the go if you can use it.

Saturday, October 01, 2005

Handling currency formatting in ASP.Net

Someone asked on the aus-dotnet list (http://www.stillhq.com/aus-dotnet/archives2/msg09751.html ) how to handle formatting of currency information in an ASP.Net application. This is something that's commonly misunderstood, so I thought I'd put something together to point people at in future.

By default all of the ASP.Net formatting takes the culture setup for the service user (ASPNET or Network Service under IIS6) - typically en-US :-(. You can explicitly override this in your web.config (or machine.config), and this is great because it centralises the configuration of how you application is going to localise. All you've got to do in the application is date.ToString("d") or decimal.ToString("C") and you're away...

... if a single deployment of your application only serves a market with a single locale, and uses that locale's currency.

However if your app always works in a set currency - say AUD - then you need to hard-code into your application so that currencies don't appear with the wrong currency symbol (that is to say, appear with the default currency symbol for the deployment local, rather than a dollar sign). The simplest way to achieve this is to override just the currency format on the thread's culture. I do this by cloning the current culture, overwriting the bits I want to and then assigning it to the thread. That way I'm not locking the app into using other settings (eg date format) purely to display a given currency symbol.

Additionally, if a single deployment of your application caters to users in multiple locales, you should probably set the culture on each thread to the end-user's culture, rather than using a single server / app.config setting. You can query the browser's settings using Request.UserLanguages. You still need to override the currency symbol as suggested above, because your app uses a single currency.

All fairly straightforward. However if a single deployment of your application has to deal in multiple currencies, then you've got a bit more work to do. You can't ever just perform a decimal.ToString("C"), because ever time an amount is formatted it has to use the currency symbol and decimal precision that's relevant for that amount's currency. The usual object-based approach is to make sure all your amounts are some kind of Money type that combines a decimal with a currency type, and implements IFormattable to format the amount appropriately. I normally provide a Registry-style lookup singleton that converts currency codes into CultureInfo's - this can be used both in the .ToString() of a Money type and when providing custom formatting during the binding cycle for a datagrid.

I make the destinction between your application and a given deployment of it because for many of these localisation issues, just flicking a setting in web.config's not going to cut it anyway - someone's got to translate the copy, remove all the culturally-specific idioms, vet that images or product names are still unoffensive etc...

Incidentally, working in a WinForms (er... 'SmartClient') environment doesn't absolve you from thinking about these issues, it just makes it easier to detect your user's settings. You still might not want to use them as is.

PS: Don't mix up Thread.CurrentCulture (sets up the formatting information), and Thread.CurrentUICulture (for localized resources (strings etc...))
Developers aren't on the skills-in-demand list in Australia at the moment, so here's another option:

Wednesday, September 21, 2005

Choose Life

Choose life. Choose inheritance. Choose a job subclassing all the controls you ever built, and all the 3rd party ones you might ever use. Choose combinatorial explosion. Choose painfully recreating designers to work against the subclasses you just built. Choose aggregated proxy-by-reflection just to get the damned sealed classes working, and waking up in a cold sweat in the middle of the night panicking over the grotesqueness of it all. Choose backing yourself into a corner. Choose starting from scratch for the next version. Choose wishing you could call base.base.method() because your immediate subclass overrode the method to do something you didn't want. Choose your future. Choose inheritance.


(me getting wound up on aus-dotnet list again)

Tuesday, September 13, 2005

CruiseControl.NET RC1 Log-merging issues with nmock via nant

Summary: using a strict mock in a unit test run using nant's nunit2
task breaks RC1 CruiseControl's merging of the nant output [update: This is apparently fixed in later builds of nmock - see comments at end]

When we migrated to the latest version of CruiseControl.NET last week
(the RC1 release), one of our builds stopped merging the logs
properly (so we started getting the dreaded 'Log does not contain any
Xml output from NAnt' error).

This was due to cruisecontrol CDATA-wrapping the nant output in the
log, but at the time I didn't understand why it would do that, and
what had changed in RC1.

It turns out that nmock has a known issue (
http://jira.truemesh.com/secure/ViewIssue.jspa?key=NMO-43 ) where
strict mocks complain when the mocks are Finalise()'d. Since this
only occurs on a GC, this error is normally swallowed by the host
application closing down, but you can see this behaviour if you run a
test including a strict mock from nunit-console.

Normally nant's XmlLogger catches everything going to StdOut and
StdErr, and wraps them nicely in its xml output to catch this kind of
stuff. Unfortunately nant's nunit2 task runs inside nant, rather than
shelling out, so the GC that causes the nmock issue doesn't happen
until nant is closing down, by which time the XmlLogger has already
closed down.

As a result, nmock writes this kind of output directly to StdErr:

Unhandled Exception: NMock.VerifyException: Finalize() called too
many times
expected:0
but was:<1>
at NMock.Mock.Invoke(String methodName, Object[] args)
at ProxyDataFormatter.Finalize()


...the <>'s in which cause CruiseControl to wrap nant's output with
CDATA and hence hide it from the dashboard.

And what's changed in RC1? Well since this wasn't an issue before I
can only infer that RC1 now sucks in StdErr *as well as* StdOut from
nant.

So... Really what it boils down to is:

- CruseControl should probably CDATA the StdErr from nant (rather
than assuming its valid Xml!)

- nmock needs fixing (seems like they're too busy playing with a v2
generic version to look at existing issues)

- tricky to see how nant's XmlLogger could cope better

- should be a FAQ on 'Log does not contain any output from NAnt'

In the intermediary we've hacked our version of nmock as per the link
up above (somewhere) so as to not raise exceptions on Finalize()

Update: This is apparently fixed in later nmock releases, though we found they also broke something else we were doing (which was probably our fault), so for expediency we kept our hack. Later versions of CruiseControl.NET may also handle this more gracefully now too.

Sunday, May 01, 2005

Someone broke my VS.IDE !

Have absolutely no idea how I managed it, but all of a sudden Visual Studio 2003 thought all my projects were Windows Applications. The .csproj files were still fine. When the solution loaded a shed load of errors like

"Load of property 'AllowUnsafeBlocks' failed."
"Load of property 'ApplicationIcon' failed."
"Load of property 'AssemblyKeyContainerName' failed."

and so on appear in the Task List.

I eventually found a solution on a SAP forum (I have never had SAP installed on this, or any other, PC) - re-register the C# project DLL

regsvr32 "C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\vcpackages\csproj.dll"

regasm "C:\Program Files\Microsoft Visual Studio .NET 2003\Common7\IDE\PublicAssemblies\VSLangProj.dll"

devenv /resetskippkgs

(from https://www.sdn.sap.com/sdn/collaboration.sdn?contenttype=url&content=https%3A//forums.sdn.sap.com/thread.jspa%3FthreadID%3D12483%26tstart%3D0
)

Thanks SAP. Hope this guy worked it out too...

Wednesday, February 23, 2005

Doh! Different versions of System.Web.dll marked with same version number!

So a unit test broke inexplicably when I put it up to the CruiseControl server, and it didn't work on a collegue's machine either. It boiled down to the fact that a System.Web.UI.WebControls.Image with no AlternateText doesn't render an ALT attribute AT ALL on my collegue's machine, whereas on mine I get an empty ALT attribute.

ie
<img src="" id="myList" alt="" border="0" />
vs
<img src="" id="myList" border="0" />

Fair enough you might say, but we've both got the same version of System.Web.dll: 1.0.5000.0 from .Net v1.1.4322

Or have we?

Well obviously not, or we wouldn't be getting different Render() output. A quick squiz with Reflector indicates my assembly has internal version number f6153c24-4a3b-4f01-85cf-33d0fb852aea and size 1257472 bytes, and my collegue has e0b58e50-995f-4d8b-9d21-af7678f61626 with size 1245184 bytes. Drilling in a little futher with the disassembler finds the culprit: an extra condition in the latter that only writes the ALT tag out if AlternateText.Length > 0.

What's the moral? I don't know, but if you can't trust people to increment their version numbers when they release new versions then god help us all.

(As an aside, because assemblies in the GAC are tracked on a version number + public key basis, these two DIFFERENT assemblies are considered identical. This is presumably why MS chose not to increment the version number: because it was such a tiny weeny change. Enough of a change, however, to be noticable, and to break my (deliberately brittle) code. Bastards. Increment the version and use an Publisher Policy like you're meant to...)

Wednesday, February 16, 2005

Doh! style.top in a scrolling dhtml container element

In IE6 (and probably others), elements nested within a scrolling container (one with overflowY=auto) act as if they're in an absolutely positioned element - ie you need to account for the container when you're setting element.style.top (so you can't - say - just sum all the offsetY's from there to the document root and add/subtract some delta value for a relative-absolute position).

(Every time I (or someone I've worked with) has tried to do something clever in DHTML it's turned round and bit them on the arse. Clearly with the demise of NS4.x things are looking up, but oh-so-slowly. And I'm a DHTML advocate)

Doh! ASPNET_SETREG access rights under IIS6

When setting registry access rights for your encrypted connection strings (you know, the ones you created with ASPNET_SETREG) don't forget to add the new IIS_WPG group if you're deploying to IIS6, since your app's probably not running as ASPNET user any more...

Doh! CruiseControl.NET with Visual SourceSafe (VSS)

Turns out that VSS 6 (even 6d) has a subtle bug in it's command-line syntax (ss.exe), such that it ignores the AM/PM indicator on the 'from' part of a date range. This plays silly buggers with CruiseControl.NET, unless you set the 24Hr clock format on whatever profile user CruiseControl.NET runs as.

Popular Posts