OdeToCode IC Logo

Mice, Usability, and Silverlight

Wednesday, September 26, 2007 by scott

There is no mouse double-click event on UIElements in Silverlight.

Some people think this is an odd omission. Silverlight's cousin WPF can fire a double-click event, so why not Silverlight? Even JavaScript can catch double-click events inside a web page (although in true web fashion, the exact behavior depends on the combination of browser version, operating system, and the current phase of the moon – see Jan Wolter's Javascript Madness: Mouse Events for all the gory details).

You can still detect a double click using a mouse up event handler and a timestamp - but should you? The omission of a double-click event wasn't because of a technical limitation – the omission is a deliberate design decision. Silverlight is built for the web, and many consider double-clicking on the web to be a usability no-no.

Here is what Peter Chng has to say:

Take this comment by about the new Yahoo! Photos site; the user laments about the interface requiring a double-click to open a full-size image rather than just a traditional single-click as on most other websites. (The double-click detection is done via JavaScript)

I'll admit that while I was first confused by this action, I thought little of that user's comment - I mean, how hard can it be to learn a simple action like that? But, after some more thinking, I've come to agree with the comment - breaking the pattern of how a user navigates on a website is not a good idea, even if it's done to try to make your website feel more like a desktop application.

The New York Times website includes a feature that launches a search (in a pop-up window) when you double click a word inside an article. Digital Inspiration believes this behavior is user unfriendly, while Dion thinks the feature is a bug.

You might also have noticed the only mouse button events in Silverlight are LEFT mouse buttons events. Edit (I haven't kept up with changes in the other lifestyle...) Macs don't have a right mouse button, and my guess is they never will (adding one now would be like admitting defeat). Exposing a right-click event could only invite more usability problems.

Silverlight has some wonderful potential. Just remember to innovate – don't aggravate.

More on Conditional Compilation in ASP.NET

Tuesday, September 25, 2007 by scott

Phil dug up an old post of mine on conditional compilation, but defining a constant in web.config didn't appear to work for him. I didn't see anything wrong with his approach, so I downloaded the solution and did some spelunking.

With this page ...

<%@ Page Language="C#" 
    
CompilerOptions="/d:QUUX" %>
...
    <div>
       <% #if BAZ %>
         BAZ in the aspx file.
      <%
#endif %>
      <%
#if QUUX %>
         QUUX in the aspx file.
      <%
#endif %>
    
</div>
...

... and this web.config ...

<system.codedom>
   <
compilers>
      <
compiler
         language="c#;cs;CSharp" extension=".cs"
         compilerOptions="/d:BAZ"
         type="Microsoft.CSharp.CSharpCodeProvider, System,
             Version=2.0.0.0, Culture=neutral,
             PublicKeyToken=b77a5c561934e089
" />
   </
compilers>
</
system.codedom>

... then the page behaves as if only the QUUX is defined.

To understand the scenario I added <compilation debug="true"> to the web.config. Debug settings leave behind a .cmdline file in the temporary ASP.NET files directory. The .cmdline file contains the exact commands to invoke the C# compiler, and the abbreviated form looked like this:

...
/t:library /utf8output
/D:DEBUG /debug+ /optimize- /nowarn:1659;1699
/d:QUUX  
...

I went in thinking the compilerOptions would be additive, but after a smack on the forehead, I realized the compiler options in the @ Page directive override the web.config compiler options. Remove the compilerOptions attribute from the @ Page directive and BAZ becomes defined.

The behavior does seem to follow the principle of least surprise, even if it did catch us off guard.

Nested Selects in LINQ to SQL

Monday, September 24, 2007 by scott

Consider the following diagram to track the votes in a contest. Each voter can register only one vote (a score from 1 to 4) for each contest entry.

For any given voter, you'll want to present a screen showing every entry in the competition, along with the score assigned to the entry by the voter (if a score exists). To fetch the data with SQL, I'd write something like:

SELECT E.EntryID, E.EntryName, E.EntryDescription, @VoterID AS VoterID,
       (SELECT V.Score
        FROM   Votes AS V
        WHERE (V.VoterID = @VoterID) AND (V.EntryID = E.EntryID)) AS Score
FROM Entries AS E

The query should yield a resultset like the following:

...
EntryID EntryName VoterID Score 13 EntryA 1 4 14 EntryB 1 2 15 EntryC 1 NULL ...

I thought the query would be difficult to express in LINQ, but I stumbled into a solution just by keeping a "SQL" mindset:

var q = from e in Entries
         
select new
         
{
               EntryID = e.EntryID,
               EntryName = e.EntryName,
               EntryDescription = e.EntryDescription,
               VoterID = voterID,
               Score = (
int?)(from v in e.Votes
                             
where v.VoterID == voterID
                             
select v.Score).FirstOrDefault()
};

The LINQ generated SQL looks amazingly similar to the hand generated SQL, which is a comforting sight!

10 Tips for Shrink-wrapping ASP.NET Applications

Tuesday, September 11, 2007 by scott

Most server-side applications run on machines under our control. We know what the application's environment will look like long before deployment.

Building and packaging an ASP.NET application to deploy inside of any corporate firewall is a bit more challenging. Here are a few lessons I've learned from five years of building a commercial web app.

  1. Document your required configuration. Use this document in the sales cycle and again before delivering the software to make sure there are no surprises.
  2. Verify the environment. Even though your document says you require SQL 2005 SP2, someone will point your software to a server with SQL Server 7.0. Small mistakes like this can eat up a surprising amount of time. Write some software that will verify versions, service packs, CPU, memory, and other environmental requirements.
  3. Two words: least privilege. In today's world, someone will challenge you over all privileges you request. You need to be in the db_creator role? Why? You need to do what on our Active Directory? Why? It's best to clearly document these required privileges and explain how you use them.
  4. Look at the licensing of third party components before looking at the feature list. As soon as you find the perfect grid control, you'll find it's too expensive to deploy. Many of the commercial controls and components you can buy for server-side software are geared less towards redistribution and more towards "per server" or "per CPU" licensing.
  5. Be creative about getting diagnostics. Log errors to help debug problems, but don't count on using the event log as it may not be well maintained. The file system is generally more reliable, and many logging components offer rolling log file options. To be proactive, consider sending diagnostic information directly back to your company – just don't use email. In my experience, sending email from a web server inside the firewall out over the Internet is only going to work about 30% of the time – corporate IT departments are just too leery. Consider hosting a web service at your place that the web app can contact over port 80, but be prepared to use a proxy server.
  6. Create and install a database maintenance plan. No one else will.
  7. Ruthlessly automate the installation. Every manual step in the installation is a problem waiting to happen.
  8. Be wary of virus scanners. Many corporate departments require them, but unfortunately, they can do some funky stuff to an ASP.NET application - frequent restarts and lock assemblies. Try to impress on the new owners of your application that they can exclude your directories from scanning.
  9. Be wary of impersonation. Let's say you need access to a network resource, so you place the encrypted username and password of a domain account into web.config. At runtime, you use the credentials to impersonate a domain account and reach the resource. This strategy will work well for about three months, then that corporate password change policy is enforced, and your application starts throwing mysterious errors.
  10. Stay out of the default AppPool in IIS6. Because you don't know who your neighbors are going to be. They might like to stay up late, use too much memory, play loud music, and run with a different version of the CLR.

Check Back In A Couple Days

Friday, September 7, 2007 by scott

I'll blog again as soon as this little operation finishes....

Trying Out Persistence Ignorance with LINQ (Part II)

Wednesday, August 29, 2007 by scott

In Part I, we defined an IUnitOfWork interface to avoid coding directly to the System.Data.Linq.DataContext class. IUnitOfWork gives us the opportunity to manipulate IDataSource<T> objects.

IDataSource<T> is another abstraction. When the software is running in earnest, we need to back an IDataSource<T> with a table in a SQL Server database. However, during unit testing we might prefer to have an IDataSource<T> backed by an in-memory data structure. Essentially, we want the ability to switch between a System.Data.Linq.Table<T> implementation (SQL Server) and a System.Collections.Generic.List<T> implementation (in-memory) - without changing the code and LINQ expressions in the upper layers of software.

Fortunately, a handful of interfaces define every Table<T> object. If we implement the same interfaces, we can walk and talk just like a real Table<T>.

public interface IDataSource<T> : IQueryable<T>, IEnumerable<T>,
                                 
ITable, IQueryProvider
{

}

Implementing a persistent data source, one that talks to SQL Server, is as simple as forwarding the calls to an underlying Table<T> implementation.

class PersistentDataSource<T> : IDataSource<T> where T: class
{
    
Table<T> _table = null;
    
DataContext _context = null;
  
    
public PersistentDataSource(DataContext context)
    {
        
Check.ArgIsNotNull(context, "context");

        _context = context;
        _table = context.GetTable<T>();

        
Check.IsNotNull(_table, "Could not retrieve table for "
                                +
typeof(T).ToString());
    }

    
void ITable.Add(object o)
    {
        ((
ITable)_table).Add(o);
    }

    
public IQueryable CreateQuery(Expression expression)
    {
        
return ((IQueryProvider)_table).CreateQuery(expression);
    }
    

    
// ...

Implementing an in-memory data source is a little bit trickier, but appears possible thanks to extension methods like AsQueryable on the System.Linq.Queryable class.

class InMemoryDataStore<T> : IDataSource<T>
{
    
List<T> _list = new List<T>();

    
public void Add(object entity)
    {
        _list.Add((T)entity);
    }

    
public IQueryable CreateQuery(Expression expression)
    {
        
IQueryable queryable = _list.AsQueryable();
        
return queryable.Provider.CreateQuery(expression);
    }
    
    
// ...

Of course, this naïve implementation could never support the full application, but should be suitable for the majority of isolated unit tests.

In the next post, we'll tie everything together to see how all these abstractions work together.

In the meantime, read Rick Strahl's "Dynamic Expression in LINQ to SQL". It is scenarios like the ones that Rick is presenting that make me worry that this approach will fall apart when it hits real requirements.

What’s Wrong With This Code (#17)

Tuesday, August 28, 2007 by scott

We interrupt this LINQ series with an emergency!

Well, there is no real emergency, but there hasn’t been a WWWTC for some time, so …

The following program is suppose to compress its own source code into a Program.cs.zip file, then reverse the compression and produce a Program.txt file.

The problem is: Program.txt always shows up as an empty file!

What’s wrong?

using System;
using System.IO;
using System.IO.Compression;

namespace YippyZippy
{
    
class Program
    {
        
static void Main(string[] args)
        {
            Compress(
@"..\..\Program.cs", @"..\..\Program.cs.zip");
            Decompress(
@"..\..\Program.cs.zip", @"..\..\Program.txt");
        }

        
private static void Compress(string inFileName, string outFileName)
        {
            
FileStream inStream = File.Open(inFileName, FileMode.Open);
            
FileStream outStream = File.Open(outFileName, FileMode.Create);
            
GZipStream zipStream = new GZipStream(outStream, CompressionMode.Compress);

            
try
            {
                
byte[] buffer = new byte[inStream.Length];
                inStream.Read(buffer, 0, buffer.Length);
                zipStream.Write(buffer, 0, buffer.Length);
            }

            
finally
            {
                outStream.Close();
                inStream.Close();              
            }
        }

        
private static void Decompress(string inFileName, string outFileName)
        {
            
FileStream input = File.Open(inFileName, FileMode.Open);
            
GZipStream zipStream = new GZipStream(input, CompressionMode.Decompress);
            
FileStream output = File.Open(outFileName, FileMode.Create);

            
try
            {                
                
int data = zipStream.ReadByte();
                
while (data > 0)
                {
                    output.WriteByte((
byte)data);
                    data = zipStream.ReadByte();
                }
            }
            
finally
            {
                output.Close();
                input.Close();
            }
        }
    }
}

Hint: You can fix the program by adding a single line of code.