Tuesday, August 25, 2009

Bad UI Decisions From Google

I use Google Docs.

I admit it, I find it easier to use it for SIMPLE documents than MS Word, and it's easier to have constant and consistent access to said documents across multiple computers than using a thumb drive for storage.

Well, I had a work-flow in how I used Google Docs. Mainly, anything I was currently working on would not be in a Folder. As I finished a document, I would move it to star it and then after I dealt with it (published it to a blog or whatever its point was) I would add it to the correct folders, and remove the star.

It was simple, streamlined and above all, it worked.

In fact I choose Google Docs over Zoho Writer BECAUSE of that whole "Items Not In Folders" view and that I could implement that specific work flow.

I knew at a glance what I was working on, what I had queued for posting/publishing, and when I went through the folders they only contained relevant documents to that folder that I've generated and have done what needed to be done to them.

But then in the middle of June, Google broke that.

They removed the "Items Not In Folders" view.

The view that I went to immediately upon entering Google Docs.

The view that I spent 95% of my time in Google Docs actually using (that's not counting time actually spent writing).

I believe that this was a poor UI and a poor usability decision.

And I am not alone.

There are a number of posts in the Google Docs Help forums asking what has happened to this view. The blog post on the Google Docs Blog have a number of people wondering what has happened to it, and when it would be back.

Enough so that at one point, an actual Google product manager signed into the system and posted the following (one Vijay Bangaru):

Thanks to everyone for letting us know how much you miss "Items not in folders." We definitely are aware that you miss this feature and are actively looking into what we can do. For an imperfect substitute in the meantime, you can try finding items not in folders by going to one of the views ("Shared with me", "All items", etc) and looking for items without a labeled folder indicator. You can make the visual scanning easier by assigning colors to your folders. Thank you for your patience and feedback as we transition to a new doclist.
Yes, he provided a "work-around" but that work around is basically the same thing as taking every piece of e-mail you have in Outlook, having never archived any of it, never moved any of it to an individual folder, and then visually filtering the ones without the little red flag icon set on it.

"Imperfect substitute" does not even BEGIN to describe the inherent failure that that process represents.

The thing is that I find the entire situation odd, as I cannot think of a single "programming" reason why that particular view of data had to go away. The database which stores this information should quite easily be able to generate that view, and in fact I would have it AS a View. It should be a highly simple query to find all the items that are not associated with any TAG (or even not associated with a specific tag).

What is sadder is that there are other options that would meet the needs that I have:
  • If I could have a "Default Folder" option where a newly-created or newly-shared-with-me item is automatically placed (until I remove it) then I could continue my normal work flow.

  • If i was in a Folder, and choose to create a new item, and it was automatically assigned to that folder, then even then I could make things work.
But, I can't, and I can't, and ultimately, using Google Docs has become fundamentally unintuitive for me.

It's been a month, and since they obviously have no desire to either repair the functionality or to provide a truly usable work-around; and that means that the only thing I can think of doing is going back to my thumb drive and MS Word.

Which is sad, because I genuinely liked using Google Docs.

Friday, August 7, 2009

Logging In On a Windows Application

Oh the things people will dream up.

Here's the situation: the customer wants the system to easily be able to assign rights as user/administrator to an application, but they do not want the application to automatically assume the identity of the user who is logged into the computer.

Well, after my brain reset at this odd design decision, I got to work, and began thinking.

I knew I had a few things I could take for granted. I knew I had a SQL Server, and I knew that the client had an Active Directory & a Domain Controller. So, far, so good.

The ease of assigning administrators is a blessing with the Active Directory. All I need to do is generate two Global Groups in AD one for users and one for administrators. This makes it easy to pass-through authentication against the SQL Server as well. As all I need to do is create the USER group as a SQL SERVER USER and give it the necessary permissions to run the application.

Additionally, using AD means that there's one less username/password that the user has to commit to memory; and I'm all about having to remember less stuff.

So far, life is good.

Then I stumbled.

And not just any stumble, we're talking an animal into a tar pit level stumbling here.

It was that second part of the requirement, where the user is required to log into the application itself, after they have been logged into the computer.

All right, I thought to myself, that's not a problem. I'll just query AD directly and have it authenticate.

Well, a short discussion with my Sr. Network Engineer/COO corrected that particular opinion. Not only does Microsoft discourage that type of behavior being initiated by an Enterprise-based application, but depending on how AD is configured, it may not even allow an authentication request to be processed.

Additionally, the application itself would need elevated security rights in order to make an authentication request against AD.

He then informed me that SQL Server is the proposed way to utilize AD authentication (not AUTHORIZATION mind you). This goes back to that whole Global Group as a "SQL Server Windows User" thing.

But, after banging my head for a bit, I come to find out that the connection string requires Integrated Security to be TRUE.

Which means it doesn't matter one whit what I put into the username/password textboxes, as authentication occurs based on the Windows Identity Token which is running the application making the request.

Via the Authorization process (where I ask AD if the username provided is in the expected group) would cull the login request if I provided a bad username, but there was no checking at all of the password.

So, I cried.

Like a baby.

Then had lunch.

When I got back, I saw the problem waiting for me, so I once again turned to Google, MSDN and StackOverflow.

Apparently, I'm the only person who has this particular problem.

Enter prodigious amounts of head-banging.

I considered briefly calling the client up, and asking just how certain they are about this whole double log-in scenario. But then remembered that she was on vacation, and thus out-of-contact.

So, as I expanded my searches into wider and wider realms of dealing with C# and impersonation, I found a discussion where one would be able to launch a process as a different user. This was CLOSE to what I was attempting to do. After all, I didn't care what user context the application was running under, just so long as it was running, and whoever logged into the system was authenticated.

So, I delved further into these concepts, diving deeply into the dark nether regions of namespaces such as System.Diagnostics.

And then I started noticing something. All of these designs were accessing a non-managed DLL. A little thing called "advapi32.dll."

Which has this function:

public static extern bool LogonUser(string pszUsername, string pszDomain, string pszPassword, int dwLogonType, int dwLogonProvider, ref IntPtr phToken);
Eureka!!

Well, at least somewhat.

With this I was able to pass off the authentication of the supplied username/password to Windows itself. It would then provide me a boolean value if the logon process was successful or not, and if I ever NEED it, I also have an authentication token as an IntPtr.

Additionally, according to MSDN this will continue to work in Windows 7--so I have a year or 5 in which to convince them that a single log-on is acceptable.

Ah... Data Templates, How I Love Thee....


What? You don't like them? Oh, but you will.... you will....

WPF has a number of different Templates which can be applied to controls, and the DataTemplate is the one that's used for things like ListViews, ListBoxes and ComboBoxes.

These things have great and wonderful powers. I use them a lot--mainly when I'm trying to display complex data via a ListView--complex data such as a history of addresses for an entity. Consider, an address has 4-8 fields of data associated with it: 2 or 3 Street lines, a city, a region, a postal code and possibly country and county data. In addition to the data, I want to have command buttons that allow me to perform EDIT/DELETE operations on that line of data.

Now, in a traditional ListView, all I would have to do is bind to my Address class, and override the ToString function to return the data in a single line, and the listview will display it. For this particular example, I'll use the following as my Data Object:


public class AddressViewModel : INotifyPropertyChanged
{

private string _line1;
private string _line2;
private string _city;
private string _state;
private string _postal;

public string Line1
{
get { return _line1; }
set { _line1 = value; onPropertyChanged("Line1"); }
}
public string Line2
{
get { return _line2; }
set { _line2 = value; onPropertyChanged("Line2"); }
}
public string City
{
get { return _city; }
set { _city = value; onPropertyChanged("City"); }
}
public string State
{
get { return _state; }
set { _state = value; onPropertyChanged("State"); }
}
public string Postal
{
get { return _postal; }
set { _postal = value; onPropertyChanged("Postal"); }
}

public string AddressString
{
get {
string s = "";
s = s + _line1;
s = s + "\n";
s = s + _city + ", " + _state + " " + _postal;
return s;
}
}

public event PropertyChangedEventHandler PropertyChanged;
protected void onPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}


}


The problem there is that it doesn't provide the edit controls that I like having on such complex data types. Plus, there is confusion on what action should be performed when the user selects the item.

So, I build myself the DataTemplate. I define that I want all these elements in a specific order, binding as needed. Additionally, since I have the command buttons in the data template, it's DataContext is automatically an instance of my Data Object, so I have a handle to that specific Data Object via the SENDER parameter of my Click Event.

The DataTemplate XAML is here:

<DataTemplate x:Key="AddressRepeater">
<DockPanel HorizontalAlignment="Stretch" >
<StackPanel Height="4" DockPanel.Dock="Top" HorizontalAlignment="Stretch" >
<Separator BorderBrush="Black"></Separator>
</StackPanel>
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="0.5" >
<StackPanel DockPanel.Dock="Left" HorizontalAlignment="Left"
VerticalAlignment="Top" Orientation="Vertical" >
<Button Name="cmdEdit" Style="{StaticResource ClearAutoSizeButton}"
Height="20" Width="20" MaxHeight="20" MaxWidth="20"
MinHeight="20" MinWidth="20" Click="cmdEdit_Click"
HorizontalAlignment="Center" VerticalAlignment="Top"
ToolTip="Edit This Address" Margin="5,10,5,10">
<Image Style="{DynamicResource EditImage}" />
</Button>
<Button Name="cmdDelete" Style="{StaticResource ClearAutoSizeButton}"
Height="20" Width="20" MaxHeight="20" MaxWidth="20"
MinHeight="20" MinWidth="20" Click="cmdDelete_Click"
HorizontalAlignment="Center" VerticalAlignment="Top"
ToolTip="Delete This Address" Margin="5,0,5,10">
<Image Style="{DynamicResource DeleteImage}" />
</Button>
</StackPanel>
</Border>
<TextBlock Text="{Binding AddressString}" TextWrapping="Wrap" Margin="10,5,0,0" />
</DockPanel>
</DataTemplate>


Of course, that alone will give you a very ragged edge, as the controls won't stretch across the window as expected. Each line will only take up as much room as it needs, no more, no less. Which does not give you a pretty UI. To make that DataTemplate stretch across the screen, you've got to add some information to the ListView (or it's style).

First, you need to provide the HorizontalContentAlignment value to read "Stretch" Then, you need to set the horizontalAlignment of the ItemContainerStyle to "Stretch" as well.

But it's still not quite right. If your data extends beyond the size of your window, your controls won't wrap as expected (say you're binding to a TextBlock and you expect all the text to be displayed in the control's viewable space). To fix this, you need to tell the HorizontalScrollbar to be Disabled (which in the Listview is accessed via t he ScrollViewer.HorizontalScrollBarVisibility property).

The final prettification that you'll need on the control is if you want alternating background colors. This takes two steps. The First is to add the AlternationCount to the ListView. The second is back to the ItemContainerStyle, and you will need to add a Trigger on the ItemsControl.AlternationIndex. When the Value is 0, give it one color, when it is 1, give it another. Note that you can have as many alternation colors as you'd like.

The XAML for the "prettification" is:

<ListView Name="lsvAddresses" MinHeight="250" MinWidth="250"
HorizontalContentAlignment="Stretch" AlternationCount="2"
ItemTemplate="{DynamicResource AddressRepeater}"
Style="{DynamicResource ListViewRepeaterStyle}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
SelectionChanged="lsvAddresses_SelectionChanged"
>
<ListView.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Style.Triggers>
<Trigger Property="ItemsControl.AlternationIndex" Value="0">
<Setter Property="Background" Value="#FFFFFC"></Setter>
</Trigger>
<Trigger Property="ItemsControl.AlternationIndex" Value="1">
<Setter Property="Background" Value="WhiteSmoke"></Setter>
</Trigger>
</Style.Triggers>
</Style>
</ListView.ItemContainerStyle>
</ListView>

So, in the end, once we put all these things together, we'll get ourselves a window with a ListView which displays data like this:


And of course, the Source can be downloaded here

Blog Widget by LinkWithin