Free SQL Server 2008 E-Learning from Microsoft

Microsoft are offering three free SQL Server 2008 E-Learning courses:

  • What's New in Microsoft SQL Server 2008 for Database Development
  • What's New in Microsoft SQL Server 2008 for Enterprise Data Platform
  • What's New in Microsoft SQL Server 2008 for Business Intelligence
The courses are available at http://go.microsoft.com/?linkid=8731212

New MSDN SQL Server Wiki

The Volunteer Moderators and Answerers who support the Microsoft MSDN SQL Server Forums have created a new wiki to present Solutions to Common T-SQL Problems at http://code.msdn.microsoft.com/SQLExamples. At the moment mere mortals can't update the solutions but you can leave comments.

Posted 17 April 2008 11:35 AM by jonsayce | no comments
Filed under:
This type of page is not served

One of my experiments with ASP.NET MVC involves integration with a shopping cart product built in classic ASP. Up till now I've been happily relying on Cassini (the web server built into Visual Studio) but today I began testing the integration and ran into a problem. As soon as I requested an ASP page, my server reported that "This type of page is not served."

Initially I wondered whether the default MVC web.config had somehow prevented ASP pages working, but it turns out that it's nothing to do with MVC or the web.config - Cassini only works with HTML and ASPX pages - ASP pages must be served up from IIS.

Posted 13 April 2008 10:38 PM by jonsayce | no comments
Filed under:
MVC: Intercepting Controller and HandleUnknownException

One of the recent videos on ASP.NET MVC covers the Hands On Labs from Mix. Scott Hanselman runs through a lab called the CustomControllerFactory, demonstrating how to build an intercepting controller.

Having followed the lab and tried the code I was left dazed and confused, and I wasn't the only one. Life would have been easier if the code from the labs were available, but I couldn't find it anywhere.

There seem to be two sources of confusion:

  • The video doesn't mention a vital addition to the Global.asax.cs file to change the controller factory
  • HandleUnknownAction ends up handling all actions

Getting the Lab Code Working 

Add a line to Application_Start (in Global.asax.cs) to set the framework to use the InterceptingControllerFactory instead of the DefaultControllerFactory:

Global.asax.cs changes

With this line set, any controllers instanciated will be InterceptionControllers. Without it, the default controller factory is used and the interception classes will be completely ignored.


Catching Unknown Actions

Once the lab code is working, you'll soon notice that HandleUnknownAction is catching every single action.

This happens because the InterceptionController never has any actions. It's a wrapper around a controller of the type specified in the URL (the controller with actions), but the InterceptionController itself doesn't have any actions, so every action will trigger the HandleUnknownAction method.

The best way to get your head round this is to download the MVC code from CodePlex, add the project to your solution and reference it directly. It's a great way to learn more about how the MVC framework does its stuff: in our example, the ProcessRequest method of the MVCHandler object creates a controller using our custom controller factory, so ProcessRequest is working with an InterceptionController.

ProcessRequest in the MVCHandler class

When the Execute method of the InterceptionController is called, it calls InvokeAction, which checks the actions defined on the controller. InterceptionController has none, so Execute calls HandleUnknownAction and our override fires. In the video Scott uses the override to check the actionName value and decide whether or not the controller should continue.


Base Class 

If you're trying to catch only unknown actions then an alternative way is to define a base controller class, inheriting from controller, which overrides HandleUnknownAction:


Now all of your controllers can inherit from this base controller:


The controller created in ProcessRequest will now be of the type specified in the URL and will have actions defined, so HandleUnknownAction will only fire when there really is an unknown action - there is no need to filter on the actionName anymore.

Msg 7391 - OLE DB provider SQLOLEDB was unable to begin a distributed transaction

This error occurred when running queries joining to a linked server, when the linked server was upgraded from Windows 2000 Windows Server 2003 SP2.

Server: Msg 7391, Level 16, State 1, Line 1
The operation could not be performed because OLE DB provider "%ls" for linked server "%ls" was unable to begin a distributed transaction.

Some common causes of the error are described on MSDN although this second article actually provided the solution we needed.

In our case the problem turned out to be that Windows Server 2003 has DTC network access disabled by default. Turning it on is pretty straightforward but stops the SQL Server service and can require the installation media.

The Microsoft SQL Server Support Blog has a post describing DTC settings in more detail, but we found that the default setup worked once network access had been enabled.
 

Posted 01 March 2008 11:58 AM by jonsayce | no comments
Filed under:
What motivates geeks?
I recently moved a project from .NET 2.0 to .NET 3.5 and discovered that a method I had used to deserialize some JSON had become obsolete. This prompted much debate with my colleagues about why the .NET framework continues to change.

The guys I was talking to both reckon that a large part of the motivation for Microsoft to keep changing the .NET Framework is just to make money. I guess in their view of the situation, the desire for change is being driven from the top, and Scott Guthrie and co. are coming up with changes to please their financially-minded masters.

I don't buy this. I can't imagine the .NET architects sitting round a table in Redmond saying "We have been told to change things for .NET 4.0 so our users have to upgrade - can anyone think of anything we can change?"

My view of the situation is that change is being driven from the bottom: the guys who actually design and build the Framework, who are passionate about creating the best development platform they can, are constantly hoping to improve things for the millions of developers out there using .NET.

The fact that I'm struggling to adapt to a change in JSON deserialization is very frustrating, but I have no doubt that the person who took the decision to deprecate the old method and create the DataContractJsonSerializer class did so because they thought it would be better than the JavaScriptSerializer.

Obviously the money men in Microsoft would have been pleased that the JSON guru decided to improve things, as it's one microscopic part of the reason that people will upgrade to 3.5, but I firmly believe that the changes are driven by people who are engrossed in the technical side of their job, and are desperate to make .NET as good as possible.

They are, after all, just another bunch of geeks. They may be more talented and more influential than your average bunch of geeks, but they are fundamentally just another team of geeks doing their jobs. In my experience, whilst geeks want to get paid just like anyone else, what they really want is to make things work better.
Programmatically Listing SQL Servers

Using the .NET Framework there are a variety of ways you can search for available SQL Servers, but no single technique does the whole job. The best approach is to use a combination of methods to ensure you get the most complete list possible.

Most methods rely on a UDP broadcast to locate servers - this is good for finding unknown servers which haven't been registered but doesn't guarantee to return the definitive list. Timeouts and firewalls blocking the relevant ports can lead to servers missing from the list and, if you're not on a network then even local servers will disappear from the list, as the UDP broadcast fails completely. In addition the list of servers is not guaranteed to be the same if you run the search twice.

The main alternative is to search the registry for registered SQL Servers. Whilst this is fine for servers you know about (and works when the network is unavailable) it doesn't allow you to discover new SQL Servers.


Method 1: UDP Broadcast

Used by:

The first two actually are actually the same under the covers: EnumAvailableSqlServers is simply an abstraction of the SqlDataSourceEnumerator class.

EnumAvailableSqlServers example

Pros:
+ Includes SQL Servers which are not registered

Cons:
- Doesn't work when there's no network connection
- Subject to firewall rules (Blocked TCP 1433 and UDP 1434)
- Doesn't find SQL Servers if SQL Browser is off
- Doesn't find SQL Servers if they are hidden
- List contents not guaranteed to be repeatable


Method 2: Reading Registered Servers from the Registry

Used by:
Microsoft.SqlServer.Management.Smo.SmoApplication.SqlServerRegistrations.RegisteredServers

RegisteredServers example

Pros:
+ Works with no network connection
+ Works regardless of firewalls
+ Shows all registered servers, regardless of SQL Browser/hidden status

Cons:
- Only shows SQL Servers which have been registered
- Will be removed in a future version of SQL Server according to MSDN


Method 3: SMO WMI

Windows Management Instrumentation provides a third way to locate SQL Servers.

WMI example

Pros:
+ Works with no network connection

Cons:
- Only lists SQL Server 2005 and later
- Only finds registered servers


Putting the methods together

If you're after a list which matches that shown in SSMS (or Enterprise Manager) then looking for registered servers will probably be an acceptable solution. If, on the other hand, you want to show all available SQL Servers (as in the SSMS "Browse for Servers" dialog), then your best bet is probably to combine one of the UDP methods with a list of registered servers. This will give you a reliable list of registered servers even when the network is down, and will include unregistered servers where possible.

One of the most user-friendly ways of doing this is to initially populate your control with the registered servers (which can be done very quickly) but allow the user to initiate a search for further network servers (which can take several seconds) in case they're looking for an unregistered server. You should always allow the user to type in a server name too, as the server may be available even though it's not returned by the UDP methods described above.

 
And another thing...

Depending on your requirements, you may need to include the instance names in the list. The methods I've mentioned all return instance names of the non-default instances (e.g. MyPC\SQLEXPRESS) but they handle them in different ways - the MSDN documentation gives full details.

Extending SMO Classes with Extension Methods and IEnumerable(T)

One of the simplest ways to audit changes to a table is to have a trigger that stores a copy of any row that's inserted/updated/deleted, together with some meta data such as who made the change and when they made it. There are limitations (such as several types which can't be used in the inserted and deleted tables) but often this method of auditing will be sufficient.

Because of the limitations, you will normally need to specify a column list in your trigger, and this list needs to be kept up to date if the table being audited is ever changed.

While building a tool to keep audit triggers and audit tables in synch with the tables being audited, I recently found myself trying to inherit from various sealed SMO classes. Having discovered that this was impossible, I tried a different tack: extending the SMO classes with .NET 3.5 extension methods.

As ScottGu's examples show, extension methods can be extremely simple.

I added a simple method to the Column class to expose whether the column's type was available in the inserted and deleted tables (i.e. whether the audit trigger could use it), then extended the Table class to expose a collection of these auditable columns.

IEnumerable Extension Method

The solution is actually extremely simple, but I found very little written about using IEnumerable or IEnumerable(T) within an extension method. That may be because it's so straightforward that no-one has written about it, but hopefully someone will find this useful...

One thing which would be nice in a future .NET release is extension properties - both the methods mentioned above would be more satisfactory if they were properties.

How to fix slow SMO performance

By default, when SMO queries the database for the properties of an object, it only loads the basic properties. This reduces the number of tables it needs to join in the query, so dramatically improves performance, as long as you don't need the other properties.

As an example, lets look at building a simple front end to display the tables in a database and the columns of the selected table.

SQL Server application

Using SMO to request the TableCollection, the query requests just the schema name and table name from sys.tables. This is perfect for our requirements: we can populate our list of tables using the names and we don't care about the other details. We've saved time because we didn't join to the other system tables we would have needed if we were interested in properties such as indexing, keys, partitioning etc.

Now we come to populate the column details. If we use SMO in its default mode then populating the ColumnsCollection of our table will use a simple query getting the column's ID and name, joining sys.tables with sys.all_columns:

SMO SQL

If we were only displaying the column name that that would be fine. However, our listview is going to show more than just the name, and we create our listviewitem objects using some other interesting properties of the column:

Creating ListItems

Running SQL Server Profiler you'll notice that SMO has lost its way a bit. We'd expect it to fire another query to get the extended properties, but it does the extended query once for each column! Given that the full query now joins 13 tables, it's no surprise that people complain about SMO being slower than DMO.

SetDefaultInitFields
The solution to this problem is to tell SMO what you're going to need in advance. Using SetDefaultInitFields you can tell SMO to load all extra properties or you can even specify a list of exactly which properties you're going to need.

SetDefaultInitFields is set at the server level. The simplest way to set it is just to tell it that all column properties should be loaded at once.

Using SetDefaultInitFields

Checking the SQL Profiler again reveals that the initial query is now the 13-table version, so is a little bit slower, but there are no further queries. If we were to specify the exact list of properties we were interested in then we could cut down the number of tables joined and make it even faster.

Bonus improvements: AddRange and BeginUpdate
If you do write an application which sticks the column details into a listview, there are two other ways to improve performance: the AddRange method of the listview adds a collection or array of listviewitems to the listview in one operation, so avoids the nasty flickering as the control is repainted between add operations. If you prefer to use add, then the other way to stop the flickering is to use BeginUpdate and EndUpdate.

Customising the SSMS Table Designer

Rick Strahl has a great post on making the SSMS Table Designer more keyboard-friendly by removing the need to have two panes:

Customised Table Designer 

Uninstalling an SSMS Add-in

One thing I forgot to mention in my recent posts on SSMS add-ins was how to uninstall them. Normally this can be done using the standard Windows uninstall methods (Add/Remove Programs, uninstall the MSI, etc) but sometimes during development you may find you're stuck with an unwanted add-in.

The simplest thing to do in this case is what SSMS does itself when an add-in fails to initialise properly: simply remove the registry setting that causes the add-in to be loaded. Delete the key for the add-in, which will be something like this:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\90\Tools\Shell\AddIns\Namespace.Class

This will guarantee that the add-in is never loaded again, though obviously it doesn't remove the other registry settings or the add-in files themselves.

The other problem it can leave you with is unwanted menu and toolbar items in SSMS. These can be located and removed using a similar technique to that described in section 4 of my post on building an add-in. Deleting a command should remove any menu items connected to the command.
 

Third-party IFrames in HTA Applications

I've posted before about setting Application = "Yes" on iframes in HTA applications, but if the iframe is rendered by a third-party component then this may not be straightforward. If the iframe runs JavaScript accessing it's parent window then somehow the iframe must be modified to pass the security rules enforced by the HTA model.

Using the onload event of the <BODY> element it's possible to set the Application attribute of an iframe, but this is not enough to make the iframe HTA-friendly.

Refreshing the contents of the iframe (using iframe.src = iframe.src) doesn't work either. I'm guessing that the security aspects of an iframe are baked into the element when the browser first renders it.

The solution is to use the onload event of the body to rebuild the entire iframe element:

if (document.getElementById('spIFrame')) {

  // get reference to the iFrame
  var iFr = document.getElementById('spIFrame');

  // set the application attribute of the iFrame (so it works in HTA)
  iFr.application = 'Yes';

  // rebuild the iFrame to make sure Application="Yes" is active
  iFr.outerHTML = iFr.outerHTML;
}
Building a SQL Server Management Studio Add-in
Various people have built SSMS add-ins and some have even given tips on writing one, but very few of them explain in enough detail how to start from scratch. Joseph Cooney recently published an excellent post, but by the time I found his article, I'd already written most of this one, so here's a second view on how to do it.

I suggested last year that everyone should vote for extensibility support in the next version of SSMS but then began learning more about creating add-ins without Microsoft's support, and with a bit of help from Sean Price, I recently finished an add-in to display recent queries: Total SQL Recall.

I've included a template project (DemoAddin) at the end of the post, which should help getting started.


1. Creating an Extensibility Project

The first step is to create an Exstensibility Project using the Visual Studio wizard. Since this part of the process is the same for Visual Studio and SSMS add-ins there's good documentation available on MSDN and elsewhere, and even one article aimed at SSMS add-ins, so I'm not going to go into too much detail. The important choices are:

  1. New Project dialog: Select Extensibility (under "Other Project Types") then Visual Studio Add-in, and launch the wizard
  2. Wizard Page 1: Select your language
  3. Wizard Page 2: Select Microsoft Visual Studio 2005
  4. Wizard Page 3: Fill in name & description
  5. Wizard Page 4 & 5: Ignore these options for the moment
  6. Wizard Page 6: Create the template

To switch the project from a Visual Studio to an SSMS add-in, open the My Project / Debug page. The Start External Program option needs to be changed from Visual Studio to SSMS, which by default is found at:

C:\Program Files\Microsoft SQL Server\90\Tools\Binn\VSShell\Common7\IDE\SqlWb.exe

 The /resetaddin switch is a handy feature if you're developing Visual Studio add-ins - it removes the commands installed by your add-in, effectively allowing the add-in a clean install next time you start up the IDE. Unfortunately, because SSMS doesn't support add-ins, this option is not available and we'll need to do this manually. Clear the command line options and we'll deal with this later.

Clear the working folder too, unless you need it set to something specific for your add-in.


2. References

There's extensive documentation on MSDN regarding Visual Studio's EnvDTE object model, which is at the heart of Visual Studio add-in development, and most of this applies to SSMS. Most UI elements are the same for both environments but if your add-in relates to anything SQL-specific then you're going to need references to SSMS assemblies and the documentation on these is non-existent.

As explained in the ASP Alliance article I mentioned earlier, there are two places to look for SSMS assemblies:

C:\Program Files\Microsoft SQL Server\90\SDK\Assemblies\
C:\Program Files\Microsoft SQL Server\90\Tools\Binn\VSShell\Common7\IDE\

So far I've found that all the assemblies I've needed are in the IDE folder, but it will depend on what you're doing with your add-in.

One thing which will definitely help if you're digging into the assemblies in search of obscure objects or methods is Lutz Roeder's Reflector, a fantastic little tool which lets you look inside assemblies and see what they have to offer.


3. IDTExtensibility2 Events

Once you've got the references sorted out, you're ready to start coding.

The template class that Visual Studio has created implements the IDTExtensibility2 interface, but only one of the methods has any code in it so far: OnConnection.

OnConnection "occurs whenever an add-in is loaded into Visual Studio" according to MSDN - in our case the event will fire whenever you start SSMS, once the add-in is installed.

OnConnection will probably be the most important method of the interface for your add-in, but the others can be useful if you need to save settings as the add-in is unloaded or something similar.


4. SSMS Events

Handling SSMS events is one of the trickier aspects of writing the add-in, the problem being that you don't know what events there are to handle. I suspect Reflector could help here, but the method I used was suggested by Sean.

To add a handler to an event we need to know the command to which that event belongs. The easiest way to find this is to loop through the commands and look for the name which sounds most like what you're after.

For Each com As Command In _DTE.Commands
   Debug.WriteLine(String.Format("Name={0} | GUID={1} | ID={2}", com.Name, com.Guid, com.ID))
Next

The output from this will be quite large, but it's normally possible to identify a few commands as likely suspects. The next step is to get the CommandEvents object and see what events it has, then add handlers for the ones you're interested in. For example:

_CommandEvents = _DTE.Events.CommandEvents("{52692960-56BC-4989-B5D3-94C47A513E8D}", 1)

' add event handlers for the events
AddHandler _CommandEvents.BeforeExecute, AddressOf OnBeforeExecute
AddHandler
_CommandEvents.AfterExecute, AddressOf OnAfterExecute

The _CommandEvents object must be a global variable which does not go out of scope at the end of the OnConnection method. If the _CommandEvents object goes out of scope then the registration of the delegate is lost and the handler method will not be called.


5. Installing and Debugging

As I mentioned earlier, we need to manually reset the add-in every time we build it. The easiest way I've found to do this is to add a deployment project (vdproj) and include this in the debug configuration to build and install a new MSI every time you run the code.

When you add the primary output of your add-in project, the detected dependencies will include a bunch of SQL Server DLLs which you don't need. Exclude all of these apart from the framework - the dependencies are part of SQL Server, which will obviously be installed before your add-in.

Set the Register property of the primary output to vsdrpCOM.

This takes care of building the MSI file, but we don't really want to have to install it manually every time we build it. Installing the MSI manually also means you have to build the MSI, install it, then run the project to debug, which is a bit of a drag.

The installation of the MSI can be automated using a PostBuildEvent, which allows you to debug your add-in in exactly the same way you would a normal project, simply by hitting F5. The code is compiled, the MSI built and installed, then SqlWb fires up and you're debugging. The PostBuildEvent just needs to install the newly built MSI:

msiexec.exe /quiet /i "D:\Visual Studio 2005\Projects\SSMSAddin\Debug\SSMSAddin.msi"

So far I haven't been able to find a way of automatically uninstalling the add-in (the PreBuildEvent doesn't seem to work reliably), but it must be possible. I just right click on the deployment project and manually uninstall after every run of the code.

There's one more step required before we can actually run our add-in. The MSI takes care of registering the add-in classes, but doesn't let SQL Server know about it, so we need to add a registry entry in the following key:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\90\Tools\Shell\AddIns\

Once again, the setup project can take care of this. Go to the registry section and create the entire path to the Addins key, then under the Addins key add another one telling SQL Server the namespace and class-name of the add-in. Within this final key, create a DWORD called LoadBehavior, with the value 1. You can also add a name and description:

Registry Settings

 If you're using SQL Server 2008 then the registry key is:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SQL Server\100\Tools\Shell\AddIns\

You should now be able to hit F5 and debug your add-in - you can put a breakpoint on OnConnection in the Connect class to test this. If debugging isn't working then check that the setup project is included in the debug configuration and is being built when you run the code - I had one experience where the configuration manager settings didn't seem to be working until I reset the Visual Studio settings.

If you get a message that the 'PostBuildEvent' failed with error code 'XXXX' then something is wrong with the MSI installation. There are various possibilities:

  • The MSI file has not been built because of a build error in the main project
  • The path to the MSI file is wrong
  • The add-in is already installed (uninstall is by right-clicking on the setup project and selecting uninstall)

If it's not immediately obvious what's wrong then run the PostBuildEvent in a command window, without the "/quiet" switch.


6. Add-in UI and Startup Mode

Page 4 of the Add-in Wizard asks a few questions about the UI and startup mode of the add-in.

Checking the first box (the UI question) causes the wizard to generate a Connect class which implements IDTCommandTarget as well as IDTExtensibility2. Two extra methods are created for this interface:

  • Exec: Executes the specified named command
  • QueryStatus: Returns the current status (enabled, disabled, hidden, and so forth) of the specified named command.

There's also a load of code added to the OnConnection method to create a toolbar button. This is added inside an If statement, checking whether the connect mode of the add-in is ext_cm_UISetup. UISetup indicates that it's the first time the add-in has run, so its commands need to be registered.

Whenever I've used this generated code it has been unreliable, as the connect mode never seems to be UISetup. I took an alternative approach based on an article by Rick Strahl, who sounds like he suffered from similar problems with UISetup.

OnStartupComplete fires every time the add-in loads - this can be used to create the command if necessary, and add the create the menu control. The control can then be removed when the add-in is unloaded, using the OnDisconnection method. This method is used in the DemoAddin project mentioned below.

Another article I found very useful understanding the toolbar/menu side of things was by Carlos J. Quintero.

The other two questions in the wizard are not, as far as I know, relevant for SSMS add-ins.


7. Windows

Adding windows to an add-in relies on exactly the same methods as used for Visual Studio add-ins, using user controls. This is demonstrated in the DemoAddin project.


8. An Example Project

I've created a template project which can be used as a basis for an add-in. It doesn't do much, but it should be fairly easy to get running and illustrates a few things:

  • A message box will pop up when SSMS starts to indicate that the add-in is loaded
  • A toolbar item is added to the Tools menu of SSMS, which opens a new window when clicked
  • An event fires every time you execute SQL from the query window

To be able to run the project, you should check the following settings:

  • Start External Program has the correct path to SqlWb.exe (See section 1)
  • The PostBuildEvent has the correct path to the location of the MSI file 

If these setting are correct then you should be able to run it straight away, step into the code, and start creating your own SSMS add-in.

Total SQL Recall - A Recent Queries (SQL Execution History) SSMS Add-in

Total SQL Recall in actionTotal SQL Recall is an add-in that stores and displays your recently executed SQL, fully integrated with SSMS. It provides the same SQL recall functionality in SQL Server Management Studio that TOAD users have had for years. SQL fragments can be inserted into the current query editor, opened in a new window, or dragged / copied to another environment.

The add-in is displayed as a standard SSMS tool window, so can be docked, auto-hidden or floated as required. The window lists the recent queries, together with their date of execution. The size of the rows and columns can be modified, and if a query doesn't fit then a tooltip will display more details. A filter lets you search your stored SQL queries.

Options are available to start/stop query-storing (useful if you're running some huge scripts and don't want to store them), configure the number of queries stored and adjust how they appear. More advanced options can control the maximum length of query stored and the maximum size of tooltip displayed.

The simplest way to recover a piece of SQL is to double-click on it or drag it into the current query editor. The context menu offers more options including opening in a new window and copying to the clipboard. The context menu also provides access to the options screen and the start/stop feature.

Total SQL Recall can be launched from the View menu - it will be the last item on the menu. The shortcut is Alt-V,O. If SSMS is running when you install Total SQL Recall then you'll have to restart it.

There are four install packages, depending on your O/S & version of SSMS:

It's vaguely possible that there might be a second version of the add-in with a second tab for permanently stored queries. If there are any other features you'd find useful then let me know.

Many thanks to Sean Price for pointing me in the right direction after my initial post on SSMS Add-ins.

Drag and drop problems in HTA frames
During our recent migration to HTA we discovered a problem dragging hyperlinks within frames (either <FRAMESET> or <IFRAME>). The problem was that dragging the contents of an <A> element onto a frame caused the link to be loaded in the frame - it was as if the hyperlink were being clicked.

The problem occurs when a drag ends inside a frame, regardless of whether the drag started outside the frame, within another frame, or within the same frame.

The contents of the <A> element don't seem to make any difference either: we first noticed the problem with image hyperlinks but then discovered that text behaved the same way.

The way we've solved the problem is using the ondragstart event. By handling the event it is possible to prevent the user dragging:
ondragstart="return false"
The ondragstart event occurs for both the <BODY> and the <A> involved, so there's some flexibility in where you attach the event handler. If the HTA is using an <IFRAME> then the ondragstart can be attached to the <BODY> of the parent document (the page containing the <IFRAME>). If the system is using a <FRAMESET> then the parent document will not have a <BODY> - there is no solution at the parent level in this case.

With both the <IFRAME> and <FRAMESET> variations you can attach the ondragstart to either the <BODY> or <A> of the child document (the page within the frame), though on large pages you may notice that attaching to the <A> element slows the browser down - using <BODY> is probably a safer bet.

  Parent Document Frame Contents
Normal page Dragging not possible N/A
IFrame Use <BODY> ondragstart Use <BODY> or <A> ondragstart
Frameset No solution Use <BODY> or <A> ondragstart

If you opt to put the ondragstart on the child document then you'll probably want to consider a way of applying it universally, so it affects all pages shown in the frame. In ASP.NET this could be done with a base page class, but the option we took was to use an HTC attached using an existing stylesheet: all our pages use a standard stylesheet, so this was modified to include a reference to the HTC file:

body
{
    behavior: url(PreventDrag.htc);
}
The htc file simply attaches a function to the ondragstart event:
<public:attach event="ondragstart" handler="preventDrag" />
<
script type="text/javascript">
    function
preventDrag() {
        event.returnValue = false;
    }
</script>
One thing to be careful of with the HTC reference in the stylesheet is it's path: the path is from the page using the stylesheet. The safest way to define it is probably as an absolute path or root relative path.

I suspect this solution will need modifying if we ever want to implement AJAX-style drag and drop, but for the moment it's solved our problem.

More Posts Next page »