Welcome to WindowsClient.net | Sign in | Join
in

WindowsClient.net

This Blog

Syndication

Sponsors





Tags

No tags have been created or used yet.

Archives

Bragi

June 2008 - Posts

  • Stretch the content of the ListView's cells

    A short time ago, I came across an interesting question on the MSDN forum.  The person in question had a ListView using a GridView as it's view with a couple of columns defined which use DataTemplates. One of those templates contained a control that needed to be stretched out across the entire width of the cell that it occupied. This seems like a trivial task for a common situation. Which it is in the end, you just have to know how to do it. To illustrate the situation, I have included a small code sample.

    <Window.Resources>
       <DataTemplate x:Key="cellTemplatePreviewPlayable">
          <ProgressBar  Height="14" Value="30" Maximum="100"/>
       </DataTemplate>
    
    </Window.Resources>
    
    <ListView x:Name="listView" ItemsSource="{Binding}">
       <ListView.View>
          <GridView>
             <GridViewColumn Header="Artist" 
                             DisplayMemberBinding="{Binding Path=Artist}" />
             <GridViewColumn Header="Title" 
                             DisplayMemberBinding="{Binding Path=Title}" />
             <GridViewColumn Header="Genre" 
                             DisplayMemberBinding="{Binding Path=Genre}" />
             <GridViewColumn Header="Listen" 
                CellTemplate="{StaticResource cellTemplatePreviewPlayable}"/>
          </GridView>
       </ListView.View>
    </ListView>

    I first came across a similar situation some time ago, and I must admit, it took some time, and a little bit of luck to find the proper solution, which is simply done using a custom style applied to the ListViewItems used as containers by the ListView. Here's the same example, but updated so that all items are stretched out.

    <Window.Resources>
    
       <Style TargetType="{x:Type ListViewItem}" x:Key="ContainerStyle">
          <Setter Property="HorizontalContentAlignment" Value="Stretch"/>
       </Style>
       <DataTemplate x:Key="cellTemplatePreviewPlayable">
          <ProgressBar  Height="14" Value="30" Maximum="100"/>
       </DataTemplate>
    
    </Window.Resources>
    
    <ListView x:Name="listView" ItemsSource="{Binding}"
                   ItemContainerStyle="{StaticResource ContainerStyle}">
       <ListView.View>
          <GridView>
             <GridViewColumn Header="Artist" 
                             DisplayMemberBinding="{Binding Path=Artist}" />
             <GridViewColumn Header="Title" 
                             DisplayMemberBinding="{Binding Path=Title}" />
             <GridViewColumn Header="Genre" 
                             DisplayMemberBinding="{Binding Path=Genre}" />
             <GridViewColumn Header="Listen" 
                 CellTemplate="{StaticResource cellTemplatePreviewPlayable}"/>
          </GridView>
       </ListView.View>
    </ListView>

    As you can see, all it takes is a style that sets the HorizontalContentAlignement to Stretch. At first, this seemed a little bit illogical to me, since a ListViewItem contains the entire data object while each cell in the GridView displays only a part of the content in a separate object. If you only go by the name of the property, you'd expect it to control the layout of the entire content, while actually it controls the layout of each data item separately.  Having said that, a ListViewItem combined with a GridView doesn't actually have a content to lay out, this is done by the GridView and to know how it lays out all of the columns, it uses the properties of the ListViewItem, and then it all starts to make sense, at least to me.  I hope for you to.

    Of course, when you use this style, the cells in 'all' columns are stretched out, so if you need another alignment in another column, you will have to wrap your the content of your template inside another object which defines the correct alignment.  Here's the another DataTemplate that does exactly this.

    <DataTemplate x:Key="cellTemplatePreview">
       <Border>
          <ProgressBar HorizontalAlignment="Left"  
                       Height="14" Value="30" Maximum="100"/>
       </Border>
    </DataTemplate>
  • DistributionPanel

    Intro

    Let me start by telling you that this post was actually written about 40 days ago.  Non finished code kept me from posting it, which I found a real shame. Especially, since the technique it is based on got me very excited. But I simply couldn't get me to post some code, I hadn't even been able to test yet, which I am sure you understand.  So what exactly got me so excited anyway?  Well, simply put: this.  It's an article from Dr WPF about a new panel he made called 'ConceptualPanel' which  detaches the items in the panel from it's visual tree so that they can be placed somewhere else. When I read this, I had just started looking into a similar feature (well, actually I had already begun coding it for a couple of days), but had approached my problem from a different angle: I presumed that Panels couldn't be changed in the way Dr WPF did (thank you for proving me wrong on this one), so I figured to re-implement most of the ItemsControl features but in such a way that you could send them to multiple panels.  This proved to be a nightmare to put it mildly due to the many internal functions used by the ItemsControl. All situations could be solved, but not without breaking with the logic used by the .net framework. So this article really came as a blessing and saved my but.

    What does DistributionPanel do?

    First of though: What really is the importance of this conceptual panel?  Well, from my point of view it all becomes very interesting when you start using this panel with an ItemsControl.  You see, in the past, whatever you put on an ItemsControl, had to remain in its visual tree (in other words, inside the ItemsControl) because this uses a Panel to perform it's layout and those automatically made all their Children also visual children.  This is no longer true, you can now put the containers anywhere you want, even on another window.  So the ItemsControl simply becomes a factory of container elements.  Of course, ConceptualPanel only gives you the detaching, it doesn't actually provide a way to use multiple destinations for it's children (which is the feature that I was looking for). As you probably already guessed by know, this is where DistributionPanel steps in.  It expands the ConceptualPanel so that it can distribute it's children to other lists without using code (so you can do it all from xaml).

    Yes, you read it correctly, and it isn't a typo or expression error (something I can't always claim to be true), DistributionPanel can add it's children to other lists through the IList interface - not just to Panels.  It can also remove them again when the child is removed.  Using the IList interface instead of Panels can be useful in cases where you want to put the items on something different, for instance, you can create a class that makes all items in the list floating. And since the UIElementCollection used by the Panel also implements IList, you can still use panels as distribution targets.

    Key and Targets

    Distribution is performed by using a Key and a list of Targets.  The key is assigned to each item in the list using an attached property. Each DistributionTarget declares a value that is compared to the key of each item that needs to be distributed.  The first target who's value matches with the key will be used to put the item in.  This is done by adding the item to an IList that can be found through the Target property of the DistributionTarget.

    Ouch, sounds complicated.  Actually it really isn't.  I think a code sample is appropriate by know to clear things up.

    <DockPanel>
       <jc:DistributionPanel>
          <jc:
    DistributionPanel.Targets> <jc:DistributionTarget Value="Top"
    Target="{Binding ElementName=PART_Top, Path=Children}" /> <jc:DistributionTarget Value="Left"
    Target="{Binding ElementName=PART_Left, Path=Children}"/> <jc:DistributionTarget Value="Right"
    Target="{Binding ElementName=PART_Right, Path=Children}"/> <jc:DistributionTarget Value="Bottom"
    Target="{Binding ElementName=PART_Bottom, Path=Items}"/> </jc:DistributionPanel.Targets> <TextBlock jc:DistributionPanel.Key="Bottom">Item A</TextBlock> <TextBlock jc:DistributionPanel.Key="Top">Item B</TextBlock> <TextBlock jc:DistributionPanel.Key="Left">Item C</TextBlock> <TextBlock jc:DistributionPanel.Key="Right">Item D</TextBlock> </jc:DistributionPanel> <StackPanel DockPanel.Dock="Top" x:Name="PART_Top"/> <ListBox DockPanel.Dock="Bottom" x:Name="PART_Bottom" /> <WrapPanel DockPanel.Dock="Left" x:Name="PART_Left"/> <self:CustomList/> </DockPanel>

    Basically, this is a DockPanel which has a stackPanel at the top, a listbox in the bottom, a WrapPanel to the left and some custom list object in the middle.  The DistributionPanel contains all the items which will be distributed to all the other items found in the DockPanel. Each item in the list has an attached property DistributionPanel.Key.  The DistributionPanel also has a list of targets. The first one will receive all the items in the distributionPanel who's key value equals "Top" and sends the items to the StackPanel at the bottom of the app.  The second target filters out all the items which have a Key value of 'Left', and so on.

    Uses

    Even though the distribution technique appears very simple, you can get some really cool results, especially if you combine it with the triggers. Lets say there are 2 properties that determine where an item needs to be placed.  You can use a DataTrigger that checks the value of these 2 properties and use a setter to assign the correct value to DistributionPanel.Key.

    As already mentioned, this class is probably most useful when used as the ItemsPanel of an ItemsControl (such as a ListBox or TreeView).  This way, you can easily detach the items in the ItemsControl and put them somewhere else, like during a drag operation.

    Extending/changing the distribution

    DistributionPanel has been designed with extensibility in mind.  Interesting  new features could be: support for animation during a transition of one distribution location to another one, or you could implement a placeholder kind of functionality where the panel is able to put an item back in it's original location (The current implementation of DistributionPanel looses the order of items when it moves items from one list to another).

    To extend the class, you should overwrite the protected method Distribute, which is called whenever an item is added, removed or moved from one target to another. So, if you would want to add animation functionality, this is the place to do it. All that the method does is raise the appropriate events and ask the DistributionTarget objects to add/remove the item.  Here's the implementation:

    protected virtual void Distribute(UIElement item, DistributionTarget oldTarget,
             DistributionTarget newTarget)
    {
       //raise the preview event
       DistributionEventArgs iArgs = RaisePreviewDistributedEvent(item, oldTarget, newTarget);
       //do some sanity check: if there is an oldTarget, it should be in it's list, otherwise
       //we shouldn't be able to remove it.
       //Also, if there is an oldTarget, ask it to remove the item.
       if (oldTarget != null)
       {
          if (oldTarget.Items.Contains(item) == true)
             oldTarget.RemoveChildFromTarget(item);
          else
             throw new DistributionException("Invalid old key!", new ArgumentOutOfRangeException("oldTarget"));
       }
       //if there is a new target, ask it to add the item.
       if (newTarget != null)
          newTarget.AddChildToTarget(item);
       //and finally raise the event indicating that the distribution has completed.
       RaiseDistributedEvent(iArgs);
    }

    For animation, you will probably have to re-implement this method, that is, if you want the Distributed event to be raised after the animation is done. The panel also has the Redistribute (called only when an item is moved from one location to another), RebuildDistribution (recreates the entire distribution) and ClearDistributionFor (removes all the items from a specific target list) virtual methods that you can overwrite, but scenario's to overwrite these are not that abundant.

    Besides DistributionPanel itself, you can also modify the RemoveChildFromTarget and AddChildToTarget protected methods defined in the DistributionTarget class. These methods do the actual adding and removing from the target lists.  Here are the default implementations:

    internal protected virtual void AddChildToTarget(UIElement item)
    {
       IList iList = Target;
       if (iList != null)
          iList.Add(item);
       //Items is a protected property that contains all the UIElements
       //assigned to this target.
       Items.Add(item);
    }
    internal protected virtual void RemoveChildFromTarget(UIElement item)
    {
       IList iList = Target;   
       if (iList != null)
          iList.Remove(item);
       //Items is a protected property that contains all the UIElements
       //assigned to this target.
       Items.Remove(item);
    }

    As you can see, nothing fancy, just a standard add or remove from the target list. The only thing worth noting is the second list: DistributionTarget also maintains an internal list containing all the items that it maintains.  This is used in case there is no target list assigned. So this list must also be kept in sync. You can easily extend these 2 functions by replacing the item with a dummy instead of removing it from the list, which allows you to put the item back at it's original location.

    It is my opinion that DistributionPanel provides the foundations for some interesting Controls. I hope you find good use for it, and I would be glad if you'd let me know about the cool stuff you did with it. The panel is part of my ControlFramework library, which you can download from here.

    And as a final note there are still the:

    Limitations and restrictions

    All the limitations of the ConceptualPanel on which it is based are also valid for this panel, so items can loose their order (especially when they are redistributed from one list to another, because items are always added to the back of the new target list) and if microsoft decides to change the internals of their  panels, this could brake. 

    In addition, you should keep in mind that a DistributionPanel can't be used in a cascade, so you can't send the content of one DistributionPanel to another one.  Other than that, you are free to do with it whatever you want.

  • HTML help from within WPF applications Part II

    Ok, so here's the follow-up I promised about integrating html help within WPF applications which will mostly be about the class's internals and it's methods. Most often, when I need to write smaller code units such as this one, I tend to just jump in (head first) and splurt out the code in one go.  This one however, was an exception. In fact, I was kind of reluctant at first to even write it.  You see, in a previous life, I once made a translation (or part) of the windows header files to another language, which was NO FUN. So I really wasn't looking forward to do this again for the help part. Needless to say I was very happy to find this resource on the web which is basically a complete port of the HTMLHelp api to C#.

    This low level library is  quite simple to use really.  It declares a bunch of statics and structs inside the 'HH1Interop' class, most of which you will probably never need or use. More importantly, the class also has a number of public methods which provide access to help files.  Some of which are:

    • HtmlHelp_DisplayTopic(int caller,String file): Displays a help file or a specific page of it. The first argument (caller) identifies the application that does the request.  This is used for moving focus when the help file is closed (the default implementation of the help class always sets this to 0). The 'file' argument should contain the path (or filename if it is in the same dir as the app) of the help file, followed by the name of the content page to display, using '::/' to separate file and page info. As an example, if I have an html file cold 'foo' with a page called 'bar' that I would like to display, the second argument for this method would be: "foo::/bar".
    • HtmlHelp_DisplayIndex(int caller,String file,String keyword): This method will display the index page of the help file (declared in the second argument), with the value for 'keyword' used to fill in the default search criteria. 'Caller' has the same meaning as for 'HtmlHelp_DisplayTopic'.
    • HtmlHelp_DisplaySearch(int caller,String file,ref HH_FTS_QUERY query): Displays the search page of the html file who's path is declared in the 'file' argument. 'Caller' again is the application that requests the operation and the final argument (query) can be used to declare a default search value. The Help class currently leaves the query arg empty.
    • HtmlHelp_DisplayTOC(int caller,String file): This method shows the table of contents page of the specified help file.

    Using these functions it is possible to construct a class that provides basic help access from within WPF.  There are more methods declared in the raw, low level HH1Interop class such as for showing popups, but these are currently not used by the Help class, so I wont go into these any further.

    The internal structure of the Help class can now actually be split up into 2 main parts:

    1. Some properties for assigning help info to UI elements and to assign the help file. By using attached properties, we can declare help info from xaml. The different properties have already been explained in my previous post.  There is nothing more to say really about them, they are simple attached properties with no event handlers.  The only thing to note is that both Topic and HelpFile are declared using the "FrameworkPropertyMetadataOptions.Inherits" argument value.
    2. Some functions for displaying the help content by properly calling into the low level api. These are there to make certain that all the Help class's properties are used correctly. They make certain that the correct help file is used (the default or a custom defined one) and they format the topic string.  For each method in the 'HH1Interop' that I described, there is a corresponding method in the help class + some extra methods:
      • ShowHelp(): displays the help file using the help file's default starting page.  This uses 'HH1Interop.HtmlHelp_DisplayTopic' internally to show the file.
      • ShowHelpFor(UIElement item): This method will look up the topic and help file that is assigned to the item (either directly or through property inheritance).  If both are found, it displays the topic using the 'HH1Interop.HtmlHelp_DisplayTopic' method.
      • ShowHelpTopic(string topicId): This method will show the specified topic found in the default help file, also using 'HH1Interop.HtmlHelp_DisplayTopic'.
      • ShowHelpContents(): Is used to show the table of contents page of the default help file. This is done using the 'HH1Interop.HtmlHelp_DisplayTOC'.
      • ShowHelpSearch(): Is used to show the search page of the default help file. Note that it is currently not possible to fill in a default search term, instead this is always left empty. This is done using the 'HH1Interop.HtmlHelp_DisplaySearch' method.
      • ShowHelpIndex(string start): Displays the index page, with the value for 'start' used as the initially selected index (use null if there shouldn't be a term filled in). This is done with the 'HH1Interop.HtmlHelp_DisplayIndex' method.

    There are many ways that this class can be extended.  For instance, there is no support for popups, nor for default search criteria and I am certain that seasoned help hackers know tons of stuff that's missing.  These are things I leave up to you, if you feel like it or have an itch to scratch.

Page view counter