Monday, June 12, 2006

When you are creating a layout in Xaml, you should be careful to take into account the width you have left. Yes, WPF will try to be smart and scale your controls, but what if the content of your control does not want to be scaled?

Paste this into Xaml-Pad and resize your Window:

<Grid Width="Auto" Height="40" xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
            xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
            xmlns:d='clr-namespace:System.Windows.Data;assembly=PresentationFramework' >
<Button HorizontalAlignment="Left">A lengthy button</Button>
<TextBlock HorizontalAlignment="Center">An longer TextBlock</TextBlock>
</Grid>

Nick Thuesen has a solution for this. He has created 3 rangeConverters that allow you to plug in a width and set a range where you would like your control to be visible. That is cool, I like it, I will use it.
In my specific case though, I wanted to collapse the content of a control based on the width of it's container. Basically, the container is a border, and it's content is a stack panel with some text boxes. The container is sized by an algorithm, so the container is more important then the content. The panel that does this scaling is surrounded by a slider, which zooms the controls accordingly. When enough space is available, the content should become visible.
Since I do not know the range in which the content should be shown, I can't use his controls.

It was dead simple to create my own though. Instead of using a single binding converter, I've used the MultiValueConverter to be able to bind to multiple properties.

public class ClippingToVisibilityHiddenConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
Debug.Assert(values.Length == 2);

double availableWidth = Double.PositiveInfinity;
Double.TryParse(values[0].ToString(), out availableWidth);

double desiredWidth = 0d;
Double.TryParse(values[1].ToString(), out desiredWidth); // possibly unset

double margin = 0d;
if (parameter != null)
Double.TryParse(parameter.ToString(), out margin);

Console.WriteLine("Desired: {0}, avail: {1}", values[1].ToString(), availableWidth.ToString());
return desiredWidth > (availableWidth-margin) ? Visibility.Collapsed : Visibility.Visible;
}

public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new Exception("not implemented");
}
}

Usage is equally simple:

<StackPanel.Visibility>
<
MultiBinding Converter="{StaticResource ClippingConverter}" ConverterParameter="15" >
<
Binding Path="ActualWidth" RelativeSource="{RelativeSource AncestorType={x:Type ContentPresenter}, AncestorLevel=1}" />
<
Binding Path="ActualWidth" RelativeSource="{RelativeSource Self}" />
</
MultiBinding>
</
StackPanel.Visibility>

There you go, I hope you're happy with it! Confusion though about the ActualWidth property of elements....

My custom panel that does the layout, determines the desired size of the elements by doing a measurement of the element. Then at the arrange-pass, it calls an arrange on the element with a size possibly smaller then it desires. Somehow, when you read back the ActualWidth, DesiredSize, RenderSize or what else you can think of, you will never find that passed-in size.

Well, call me st00pid, but I would have expected the RenderSize to let me get to the .... rendered size..  ;-)
It does not.

Since I so desperately do want to find that 'clipped' size, I set the render size myself, after the arrange:

Size s = new Size(days * pixelsPerDay, uie.DesiredSize.Height);
uie.Arrange(new Rect(new Point(tsLinks.Days * pixelsPerDay, 0), s));
uie.RenderSize = s; // oh dear

It's definition in the SDK: Gets (or sets, but see Remarks) the final render size of this element.
So, the remarks then: Do not attempt to set this property, either in XAML or in code, if using the Windows Presentation Foundation (formerly code-named "Avalon") framework-implemented layout manager (nearly all common application scenarios will be using this framework layout manager). The layout manager will not respect sizes set via this property directly...

Yikes.

I like feeling naughty, but feeling this naughty can't be good.
I'll keep it in though, until I find a way to bind to the actual ActualWidth.

Monday, June 12, 2006 2:38:30 PM (Romance Standard Time, UTC+01:00)  #    Comments [0]  |  Trackback

Everybody (and their grandmother) has blogged about the name change of winfx to .Net Framework 3.0. Their grievances are mostly focused on the weird dependencies that are created: framework 3.0 relies on framework 2.0 to be installed?! Also, winfx is perceived as a very cool name by the masses (me included!).

But when you think about it, this also allow you to tell your clients: 'you need the .Net Framework 3.0 installed to run this incredible piece of software'. They will also be much more inclined to install an update, versus an add-on.
So, it is very confusing, but in the end maybe for the best.

Brad Adams is answering questions.

Monday, June 12, 2006 1:41:14 PM (Romance Standard Time, UTC+01:00)  #    Comments [1]  |  Trackback
 Thursday, June 08, 2006

I always use Expression Interactive Designer when I want to see the full default style of a control, but here I have found a equally easy way: just dump it into XML.

Style style = FindResources(typeof(Button)) as Style; //Get button's default Style
if (style != null)
{
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.IndentChars = new string(' ', 4);
StringBuilder strbuild = new StringBuilder();
XmlWriter xmlwrite = XmlWriter.Create(strbuild, settings);
XamlWriter.Save(style, xmlwrite);//Use XamlWriter to dump the style
return strbuild.ToString();
}

It uses the findresources method to find a style and then just writes it out. Might come in handy!

Thursday, June 08, 2006 7:43:13 PM (Romance Standard Time, UTC+01:00)  #    Comments [4]  |  Trackback
 Wednesday, June 07, 2006

My previous post was about not being able to bind to a named element outside of your scope. With the help of some great people, we figured out a workaround: you can bind to an ancestor. So, this does work:

<StackPanel DataContext="{Binding ElementName=lb, Path=SelectedItem}">
<
ListBox Name="lb" ItemsSource="{Binding Source={StaticResource InventoryData}, XPath='Book'}" IsSynchronizedWithCurrentItem="True" />
<
local:CustomItemsControl ItemsSource="{Binding RelativeSource={RelativeSource AncestorType={x:Type StackPanel}}, Path=DataContext}" />
</StackPanel>

This has a listbox that binds to a datasource (xml this time) and a stackpanel above it binds it's datacontext to the selecteditem of the listbox.
Obviously this has all kinds of nasty smells surrounding it.... Next please!

We might not be able to bind to a named element outside of scope, but if we can bind to an ancestor, chances are good that we can bind to a staticResource as well. So let's introduce a CollectionView around our XML datasource. In the Window.Resources:
<CollectionViewSource x:Key="InventoryView" Source="{Binding Source={StaticResource InventoryData}, XPath='Book'}" />

And then we can bind to the currentitem of this view, instead of the ugly datacontext of the parent stackpanel:

<StackPanel >
<
ListBox Name="lb" ItemsSource="{Binding Source={StaticResource InventoryView}}" IsSynchronizedWithCurrentItem="True" />
<
local:CustomItemsControl ItemsSource="{Binding Source={StaticResource InventoryView}, Path=CurrentItem}" />
</
StackPanel>

This is a good enough workaround for now. ;-)

Wednesday, June 07, 2006 7:40:44 PM (Romance Standard Time, UTC+01:00)  #    Comments [2]  |  Trackback