Showing posts with label WPF. Show all posts
Showing posts with label WPF. Show all posts

Thursday, September 15, 2011

Design-time support for Caliburn.Micro

Recently me together with Bartek Pampuch have been wondering if it’s possible to fire Caliburn.Micro’s binding process at design-time. Caliburn.Micro is a really great framework (just take a look at some of our multitouch apps based on this framework and BFSharp). You just name the controls appropriately and all magic happens automatically. Controls are bound to the View Model’s properties and methods for you. The problem is that it is all happing at run time not at design time. When you are preparing the form in the Visual Studio or Expression Blend you aren’t able to see how the form will look like with bound data. In some cases such feature would very useful, especially when we want to use the sample data generated by Expression Blend or Visual Studio designers. In this post we will show you how to run Caliburn.Micro’s conventions based binding mechanism at design time.

Firstly, we discovered that we can change the objects tree representing the screen and that change is not persisted back to xaml file. You can read more about this feature in my previous post. Secondly, we tried to run Caliburn.Micro’s binding process to see what happens. What was really amazing is that after first try all just started working smoothly! The framework itself is implemented so well :)

One of the samples provided with Caliburn.Micro release is a very simple application called GameLibrary. AddGameView.xaml file defines the screen that looks like this:

image

After setting up sample data generated by Visual Studio and setting attached property 'DesignTime.Enable=”True” the same form looks like this:

clip_image001

In the simplest scenario all we need to run Caliburn.Micro binding process at design time is setting design time data context and enabling binding via Enable attached property. In sample above we are setting sample data generated by Visual Studio because the AddGameViewModel class doesn’t provide default constructor. Of course we could always add a default constructor in code and define view model instance in xaml file.

I chose this particular form intentionally because it contains the control that hasn’t defined custom binding convention by default. The Rating control is responsible for displaying Rating property value using stars. Rating property value is 0.8 so 4 stars should be displayed. In such cases where we are using controls with custom binding convention we need to register those conventions at design time. Any application using Caliburn.Micro has a type inherited from Bootstrapper type. This type is a starting point of our application and the instance of that type very often is defined as a resource inside App.xaml file. In fact Caliburn.Micro framework is already prepared for design time scenarios because Bootstrapper type contains virtual method called StartDesignTime. Let’s override this method and register appropriate convention:


public class Bootstrapper
{
public Bootstrapper()
{
if (Execute.InDesignMode) StartDesignTime(); else StartRuntime();
}
}

public class Bootstrapper<TRootModel> : Bootstrapper
{
}

public class AppBootstrapper : Bootstrapper<IShell>
{
private static bool isInitializedInDesignTime = false;

protected override void StartDesignTime()
{
base.StartDesignTime();

if (isInitializedInDesignTime)
return;
isInitializedInDesignTime = true;

ConfigureConventions();
}


void ConfigureConventions()
{
ConventionManager.AddElementConvention<Rating>(Rating.ValueProperty, "Value", "ValueChanged");
}
}

The instance of AppBootstapper type can be created many times at design time so boolean flag ensures that the conventions will be registered only once. When we compile the project and reopen the form we will see something like this:

image

This form represents a very simple scenario where the view model type has only properties of simple types like: string, double. boolean. It doesn’t contain any property of collection type or other view model type. In such scenarios Caliburn.Micro can find the appropriate type of view (control type) based on the type of view model. Let’s see another form called ResultsView.xaml to demonstrate this case:

image

This is a typical problem when we work with Caliburn.Micro and we cannot see anything at design time because the control is entirely collapsed :). Let’s see what happen after connecting sample data.

image

The list contains some elements but the font color is white so we aren’t able to read it because of the default Visual Studio background color. We can try to open this form inside Expression Blend where the background is dark or we can use custom design time attributes presented in the previous post.

image

Now we can see that Caliburn.Micro is not able to find view for view model type representing list item. It’s because we are using sample data mechanism from Visual Studio which generates dynamic type _.di0.GameLibrary.ViewModels.IndividualResultViewModel with the shape of the original view model type GameLibrary.ViewModels.IndividualResultViewModel. We need to change the way Caliburn.Micro is searching for view type based on specified view model type.

protected override void StartDesignTime()
{
base.StartDesignTime();

if (isInitializedInDesignTime)
return;
isInitializedInDesignTime = true;

ConfigureConventions();

AssemblySource.Instance.AddRange(new[] { typeof(App).Assembly });

var originalLocateTypeForModelType = ViewLocator.LocateTypeForModelType;
Func<Type, bool> isDesignTimeType = type => type.Assembly.IsDynamic;
ViewLocator.LocateTypeForModelType = (modelType, displayLocation, context) =>
{
var type = originalLocateTypeForModelType(modelType, displayLocation, context);
if (type == null && isDesignTimeType(modelType))
{
if (modelType.Name == "IndividualResultViewModel")
{
type = typeof(IndividualResultView);
}
}
return type;
};

IoC.GetInstance = base.GetInstance;
IoC.GetAllInstances = base.GetAllInstances;
}

Finally the list of items and generated sample data look like this:

image

Attached property is not the most convenient way of extending the designer functionality because we need to write xaml code. I tried to rewrite attached property to custom behavior so we could use dra&drop instead of writing the code. The problem is that behaviors code is not executed at design time. But I have good news. If you are creating UI with Expression Blend you can use our attached property on the property grid like a normal property.

image

It was possible thanks to the usage of attribute called AttachedPropertyBrowsableForTypeAttribute decorating attached property. Everything that was presented so far works both in Silverlight and WPF environments. It’s time to reveal how the magic works.



public static class DesignTime
{
public static DependencyProperty EnableProperty =
DependencyProperty.RegisterAttached(
"Enable",
typeof(bool),
typeof(DesignTime),
new PropertyMetadata(new PropertyChangedCallback(EnableChanged)));

#if !SILVERLIGHT && !WP7
[AttachedPropertyBrowsableForTypeAttribute(typeof(DependencyObject))]
#endif
public static bool GetEnable(DependencyObject dependencyObject)
{
return (bool)dependencyObject.GetValue(EnableProperty);
}


public static void SetEnable(DependencyObject dependencyObject, bool value)
{
dependencyObject.SetValue(EnableProperty, value);
}

static void EnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!Execute.InDesignMode)
return;

BindingOperations.SetBinding(d, DataContextProperty, (bool)e.NewValue ? new Binding() : null);
}

private static readonly DependencyProperty DataContextProperty =
DependencyProperty.RegisterAttached(
"DataContext",
typeof(object),
typeof(DesignTime),
new PropertyMetadata(new PropertyChangedCallback(DataContextChanged))
);

private static void DataContextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!Execute.InDesignMode)
return;

object enable = d.GetValue(EnableProperty);
if (enable == null || ((bool)enable) == false || e.NewValue == null)
return;

var fe = d as FrameworkElement;
if (fe == null)
return;

ViewModelBinder.Bind(e.NewValue, d, string.IsNullOrEmpty(fe.Name) ? fe.GetHashCode().ToString() : fe.Name);
}
}

I hope you find this solution useful.

Download

Custom design-time attributes in Silverlight and WPF designer

We all know the standard design-time attributes in the Silverlight designer like d:DataContext, d:DesignWidth or d:DesignHeight. Let’s see how easy we can add some new attributes.

image
using System.ComponentModel;
using System.Windows;

namespace DesignTimeProperties
{
public static class d
{
static bool? inDesignMode;

/// <summary>
/// Indicates whether or not the framework is in design-time mode. (Caliburn.Micro implementation)
/// </summary>
private static bool InDesignMode
{
get
{
if (inDesignMode == null)
{
var prop = DesignerProperties.IsInDesignModeProperty;
inDesignMode = (bool)DependencyPropertyDescriptor.FromProperty(prop, typeof(FrameworkElement)).Metadata.DefaultValue;

if (!inDesignMode.GetValueOrDefault(false) && System.Diagnostics.Process.GetCurrentProcess()
.ProcessName.StartsWith("devenv", System.StringComparison.Ordinal))
inDesignMode = true;
}

return inDesignMode.GetValueOrDefault(false);
}
}

public static DependencyProperty BackgroundProperty = DependencyProperty.RegisterAttached(
"Background", typeof(System.Windows.Media.Brush), typeof(d),
new PropertyMetadata(new PropertyChangedCallback(BackgroundChanged)));

public static System.Windows.Media.Brush GetBackground(DependencyObject dependencyObject)
{
return (System.Windows.Media.Brush)dependencyObject.GetValue(BackgroundProperty);
}
public static void SetBackground(DependencyObject dependencyObject, System.Windows.Media.Brush value)
{
dependencyObject.SetValue(BackgroundProperty, value);
}
private static void BackgroundChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!InDesignMode)
return;

d.GetType().GetProperty("Background").SetValue(d, e.NewValue, null);
}
}
}

Great, but what about many others very useful properties ? Do we need to write them all manually? No, we don’t. I have prepared T4 template that generates code for all properties of all controls available in WPF and Silverlight assemblies. When you look carefully at the code above you will find that I am using the reflection to set the value of the property. It’s because some properties like Background are defined many times in many different controls so instead of checking the actual control type I assume that the control contains appropriate property. This assumption simplifies the implementation very much. And of course the property can be set only in design time when we are editing the form inside Expression Blend or Visual Studio. T4 template code analyzes all properties from all controls and properties with the same names but different types are skipped during generation process.

Download

Thursday, November 12, 2009

Linq to ICollectionView

Currently I'm working on the project written in Silverlight and I have encountered an interface called ICollectionView and its standard implementation PagedCollectionView. In short, this components allow us to create the view of collection of items with filtering, grouping and sorting functionality. The idea behind this interface is very similar to DataView/DataTable mechanism. DataTable is responsible for storing data and DataView is just an appropriately configured proxy (only filtering and sorting in this case) which can be bound to UI controls. Let's look at a very simple example:

public class Number
{
    public int Value { get; set; }
    public int Random { get; set; }

    public static Number[] GetAll()
    {
        var random = new Random();
        return
            (from n in Enumerable.Range(1,5)
             from m in Enumerable.Repeat(n, n)
             select new Number {Value = n, Random = random.Next(10)}).ToArray();            
    }
}

// Silverlight
public class LinqToICollectionView : UserControl
{
    public LinqToICollectionView()
    {
        Number[] numbers = Number.GetAll();

        Content = new StackPanel
        {
            Orientation = Orientation.Horizontal,
            Children =
            {
                new DataGrid { ItemsSource = new PagedCollectionView(numbers).SetConfiguration()},
            }
        };
    }
}

public static class Configurator
{
    public static ICollectionView SetConfiguration(this ICollectionView view)
    {
        // filtering
        view.Filter = (object o) => ((Number)o).Value < 5;
        // grouping
        view.GroupDescriptions.Add(new PropertyGroupDescription("Value"));
        // sorting
        view.SortDescriptions.Add(new SortDescription("Value", ListSortDirection.Descending));
        view.SortDescriptions.Add(new SortDescription("Random", ListSortDirection.Ascending));
        return view;
    }     
}

image

But wait a minute, we said ... filtering, ordering, grouping ? Let's use LINQ query to configure ICollectionView:

public static class Configurator
{
    public static ICollectionView SetConfigurationWithLinq(this ICollectionView view)
    {
        var q =
            from n in new View<Number>()
            where n.Value < 5
            orderby n.Value descending, n.Random
            group n by n.Value;
        q.Apply(view);
        return view;
    }        
}

The whole implementation consists of 3 simple classes: View<T>, OrderedView<T> and GroupedView<T>.

public class View<T>
{
    public IEnumerable<GroupDescription> GroupDescriptions { get { ... } }
    public IEnumerable<SortDescription> SortDescriptions { get { ... } }
    public Func<T,bool> Filter { get { ... } }
    
    public View<T> Where(Func<T, bool> func) { ... }
    public SortedView<T> OrderBy<T2>(Expression<Func<T, T2>> func) { ... }
    public SortedView<T> OrderByDescending<T2>(Expression<Func<T, T2>> func) { ... }
    public GroupedView<T> GroupBy<T2>(Expression<Func<T, T2>> func) { ... }
    
    public void Apply(ICollectionView collectionView) { ... }
}
public sealed class SortedView<T> : View<T>
{    
    public SortedView<T> ThenBy<T2>(Expression<Func<T, T2>> func) { ... }
    public SortedView<T> ThenByDescending<T2>(Expression<Func<T, T2>> func)  { ... }
}
public sealed class GroupedView<T> : View<T>
{
    public GroupedView<T> ThenBy<T2>(Expression<Func<T, T2>> func) { ... }
}

Methods for sorting and grouping which take an expression tree as a parameter analyze the tree looking for indicated members (fields or properties) and collect appropriate SortDescription and GroupDescription objects. Where method takes a delegate type which is combined via logical and operator with existing filter delegate set previously (in case when Where method is called many times). Of course the same mechanism work also in WPF.

Number[] numbers = Number.GetAll();

// WPF
new Window
{
    Content = new StackPanel
    {
        Orientation = Orientation.Horizontal,
        Children =
        {
            CreateListView(new CollectionViewSource { Source = numbers }.View.SetConfiguration()),
            CreateListView(new CollectionViewSource { Source = numbers }.View.SetConfigurationWithLinq())
        }
    }
}
.ShowDialog();


private static ListView CreateListView(ICollectionView view)
{
    return new ListView
    {
        GroupStyle = { GroupStyle.Default },
        View = new GridView
        {
            Columns =
            {
                new GridViewColumn { Header = "Value", DisplayMemberBinding = new Binding("Value") },
                new GridViewColumn { Header = "Random", DisplayMemberBinding = new Binding("Random") },
            }
        },
        ItemsSource = view
    };
}

At the end I'd like to mention one interesting thing. We only support filtering, sorting and grouping and don't support for instance projection, joining and so on. That's way this code should compile:

var v1 = // only filtering specified
    from n in new View<Number>() 
    where n.Value < 5 
    select n;

var v2 = // grouping (last grouping definition overrides previous ones)
    from n in new View<Number>()
    group n by n.Random into s 
    group s by s.Value;

but this will not:

var v3 = // joining is not supported
    from p in new View<Number>()
    join pp in new[] { 1, 2, 3, 4 } on p.Random equals pp
    select p;

var v4 = // projection is not supported
    from p in new View<Number>()
    where p.Value > 5
    select p.Random;

var v5 = // at least one filtering, grouping or sorting definition must be specified
    from p in new View<Number>()
    select p;

var v6 = // Numer type is the only valid type of grouped element
    from p in new View<Number>()
    group p.Random by p.Value;

As a homework I leave you a question: why it works this way ? :)

Sources