Search
On this page
Archives
RSS 2.0 Categories
Blogroll
Disclaimer
Powered by: newtelligence dasBlog 2.0.7226.0
The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.
Send mail to the author(s) E-mail
Silverlight UI Rant #2 - ListBoxItem#

Tonight's recipient of my UI Rant is the Silverlight 2 ListBox, or more specifically, the ListBoxItem.  A client recently asked me to provide an alternating row style like the DataGrid for the ListBox.  Now, if you've ever tried to add a border or background to your ListBoxItem, you've seen this:

DefaultlListBox 

No problem, you say, I will simply set the HorizontalContentAlignment of my ListBox to Stretch and all should be good.  When that doesn't do it, you'll probably try creating/editing the ListBox's ItemTemplate....maybe adding a Grid and setting its HorizontalAlignment to Stretch.  This too will fail. 

Turns out, the problem is not with your ItemTemplate or even with the ListBox itself.  The issue is in the container that actually contains your Item and that is the ListBoxItem.  The property on the ListBox that allows you to set a custom style for this ListBoxItem is called ItemContainerStyle.  Armed with this information, you quickly throw together a style for your ListBoxItem, maybe like so: 

<Style TargetType="ListBoxItem" x:Key="ItemContainerStyle">
   <Setter Property="Padding" Value="3" />
   <Setter Property="HorizontalContentAlignment" Value="Stretch" />
   <Setter Property="VerticalContentAlignment" Value="Top" />
   <Setter Property="Background" Value="Transparent" />
   <Setter Property="BorderThickness" Value="1" />
</Style>

And then add that to your ListBox, like so:

<ListBox Margin="8,8,10,8" x:Name="defaultListBox" ItemsSource="{Binding}"
   ItemTemplate="{StaticResource EmployeeItemTemplate}"
   ItemContainerStyle="{StaticResource ItemContainerStyle}"/>

Hit F5 and just about the time you congratulate yourself on solving this, the Silverlight Loading Animation will finish and you'll discover that your ListBox still looks exactly like the original screenshot above.  So, what happened? 

What happened is that the default ControlTemplate for the ListBoxItem has its HorizontalAlignment hard-coded to Left and no matter what you do when setting the properties, it will always be Left.  If you'll do a little diving into the full default style for ListBoxItem, courtesy of SilverlightDefaultStyleBrowser (highly recommended), you'll see exactly what I'm talking about in the ContentPresenter's HorizontalAlignment property:

ListBoxItemDefaultStyle

The good news is that because you have the ability to replace the Template of any control in Silverlight, you can simply "correct" this hard-coded problem by replacing the hard-coded Left value for HorizontalAlignment with {TemplateBinding HorizontalContentAlignment}, so your final new Style would look like this:

<Style TargetType="ListBoxItem" x:Key="StretchedItemContainerStyle">
    <Setter Property="Padding" Value="3" />
    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
    <Setter Property="VerticalContentAlignment" Value="Top" />
    <Setter Property="Background" Value="Transparent" />
    <Setter Property="BorderThickness" Value="1" />
    <Setter Property="TabNavigation" Value="Local" />
    <Setter Property="Template">
       <Setter.Value>
         <ControlTemplate TargetType="ListBoxItem">
             <Grid Background="{TemplateBinding Background}">
                <vsm:VisualStateManager.VisualStateGroups>
                <!-- removed VSM code for brevity  -->
                </vsm:VisualStateManager.VisualStateGroups>
                <Rectangle x:Name="fillColor" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"  />
                <Rectangle x:Name="fillColor2" Opacity="0" Fill="#FFBADDE9" IsHitTestVisible="False" RadiusX="1" RadiusY="1"  />
               <ContentPresenter x:Name="contentPresenter" Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" Margin="{TemplateBinding Padding}"  />
              <Rectangle x:Name="FocusVisualElement" Stroke="#FF6DBDD1" StrokeThickness="1" Visibility="Collapsed"  RadiusX="1" RadiusY="1"  />
           </Grid>
       </ControlTemplate>
       </Setter.Value>
    </Setter>
</Style>

Now your ListBoxItem will respect the value you are setting for its HorizontalContentAlignment in your Setter above and your ListBox will finally look like this:

CorrectedListBox

Hope that saves someone a little frustration.  I'll continue on to adding the alternating row style in another post. 

Here is the live sample.

Here is the source code.       

Thursday, February 12, 2009 11:43:12 PM (GMT Standard Time, UTC+00:00)
  Comments [0]  | 
Silverlight UI Rant #1 - DataGrid Last Column Fill#

If you've used the the Silverlight 2 DataGrid, you've no doubt seen this:

silverlightDatagrid01

I really hate when this happens!  I've seen several forum discussions where folks were looking to get rid of this nastiness, so I know I'm not the only one losing sleep over it.  I haven't come across a solution in anything I've seen and so now that I have one, I thought I'd share it. 

I ran into this a few weeks back and tried most of the obvious things with defining the DataGridColumns myself rather than auto-generating them.  In these defined DataGridColumns, I tried setting the Width of the last column to "*" just like you would a standard Grid.ColumnDefinition if you wanted to have it take up the remainder of the space.  This did nothing more than invite my buddy, AG_E_PARSER_BAD_PROPERTY, to show up...again.  It appears that star-sizing isn't yet implemented for DataGridColumn derivatives. 

This morning I was finally able to grab a few minutes to dive into DataGrid and find out what I could do about this.  I decided to create an ExtendedDataGrid and add a LastColumnFill DependencyProperty similar to way the DockPanel has a LastChildFill. Most of the work is happening in this method which is called if the LastColumnFill is true:

private void AdjustLastColumnWidth(Size finalSize)
{
    // get the Vertical ScrollBar
    ScrollBar scrollBar = this.GetTemplateChild("VerticalScrollbar") as ScrollBar;

    // compute the width to allow for the scrollbar based on it's visibility.
    double scrollBarWidthAllowance = (scrollBar != null && scrollBar.Visibility == Visibility.Visible) ? scrollBar.ActualWidth + 2 : 2;

    // compute the width of all the columns excluding the last one
    var widthOfAllButLastColumn = this.Columns
                   .TakeWhile(c => c != Columns.Last() &&
                                            c.Visibility == Visibility.Visible)
                   .Sum(c => c.ActualWidth);

     // set the last column width    
     this.Columns.Last().Width = new DataGridLength(finalSize.Width - widthOfAllButLastColumn - scrollBarWidthAllowance);
}

This is the result:

silverlightDatagrid02

Much better!  The live demo shows it with and without a vertical scrollbar.  Note that it also handles auto-generated DataGridColumns if that's your thang.

Here's the live demo.

Here's the code.

Friday, February 06, 2009 2:15:10 AM (GMT Standard Time, UTC+00:00)
  Comments [0]  | 
HtmlPage.PopupWindow vs. HtmlWindow.Navigate#

I was recently looking at various ways of launching an external window from inside a Silverlight 2 application.  Dusting off your javascript brain cell, you'll recall that to open a popup dialog in "the old days", we'd use window.open().  I knew about the HtmlBridge available in Silverlight 2 and I had used it for several other nifty interactions between my managed code and the DOM.  So, I went looking for an equivalent to window.open() and came across the HtmlPage.PopupWindow() method.  If you'll recall the javascript window.open() method is defined as such:

window.open( [sURL] [, sName] [, sFeatures]);

The HtmlPage.PopupWindow() is defined as:

HtmlPage.PopupWindow(string navigateToUri, string target, HtmlPopupWindowOptions options);

Perfect!  ...or so I thought.  I fully expected that if I called PopupWindow() like this:

HtmlPopupWindowOptions options = new HtmlPopupWindowOptions()
            {
                Directories = false,
                Location = false,
                Menubar = false,
                Status = false,
                Toolbar = false,
            };
HtmlPage.PopupWindow(new Uri("http://www.wintellect.com, "_blank", options);

..that I'd get a new browser window with the default width/height on the monitor that the launching browser is on.

After all, if I called window.open() with equivalent arguments as follows that's what would happen:

string features = String.Format("directories=no,location=no,menubar=no,status=no,toolbar=no");
HtmlPage.Window.Navigate(new Uri("http://www.r2musings.com, "_blank", features);   

Instead, I get a small window that launches on Monitor 1.  I do my main work on my Monitor 2 and I kept waiting for my popup window only to realize that it was on the other monitor.  I really hate applications that do this!

Looking into Reflector, the issue seems to be that if options is passed as null in the last parameter of PopupWindow(), the call is simply forwarded to window.open().  But, if you pass an instance of HtmlPopupWindowOptions (even with no properties set) to PopupWindow(), the ToFeaturesString() that gets called to convert this strongly-typed version of the features to the string that is needed for window.open() is also changing the height/width/top/left of the popup window. 

Looking over the documentation...yes, I'm male and sometimes do that *after* nothing else works. ....anyway, there it is in the remarks of HtmlPage.PopupWindow().  All the ugly details and the admission that my popup would be altered.  Documented or not, it's not acceptable to my client and I'm not putting my name on anything that launches a browser on the wrong monitor!    

That's when I decided to have another look at HtmlWindow.Navigate() which seems to more accurately mirror the behavior that I would expect out of something that purports to wrap window.open().  Going this route, we lose the strong typing of the HtmlPopupWindowOptions, but in exchange we get exactly what we are expecting when calling window.open().  The call using HtmlWindow.Navigate() looks like this:

    string features = "directories=no,location=no,menubar=no,status=no,toolbar=no";
    HtmlPage.Window.Navigate(new Uri("http://www.r2musings.com, "_blank", features);   

This will fire a new window on the correct monitor and will honor the default settings for width/height/top/left. 

Here's the code.

Here's a live sample.

Wednesday, February 04, 2009 4:15:15 AM (GMT Standard Time, UTC+00:00)
  Comments [0]  | 
All content © 2010 , Rik Robinson