Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically Generate simple Views from ViewModels in WPF?

Tags:

c#

mvvm

wpf

I'm slowly learning WPF using this article and other resources.

I am focusing on the application logic - defining the model + viewModel, and creating commands that operate on these. I have not yet looked at the view and the .xaml format.

While I am working on the logic, I want to have a view that can render any viewModel I bind to it. The view should

  • Render any public string properties as text boxes, and bind the text box to the property
  • Render the name of the property as a label.
  • Render any public 'Command' property as a button, and bind the button to the command (perhaps only if the command takes no arguments?)

Is something like this possible while maintaing the MVVM design pattern? If so, how would I achieve it? Also, the article suggests to avoid using .xaml codebehind - can this view be implemented in pure xaml?

like image 348
Oliver Avatar asked Apr 13 '12 11:04

Oliver


4 Answers

I don't think it is possible in XAML only. If you want to generate your views in runtime then you have to just use reflection over your ViewModels and generate controls accordingly. If you want to generate views at compile time then you can generate xaml files from your ViewModels at build time with some template engine (like T4 or string template) or CodeDom. Or you can go further and have some metadata format (or even DSL) from which you will generate both models and views and so on. It is up to your app needs.

And also in MVVM code-behind is Ok for visual logic and binding to model/viewmodel that can't be done in XAML only.

like image 68
Nikolay Avatar answered Sep 22 '22 00:09

Nikolay


I'm not sure this is an appropriate use for a "pure MVVM" approach, certainly not everything is going to be achieved simply by binding. And I'd just throw away the idea of avoiding using code-behind for your "view" here, this is an inherently programmatic task. The one thing you should stick to is giving the ViewModel no knowledge of the view, so that when you replace it with the "real thing" there is no work to do.

But certainly seems reasonable thing to do; it almost sounds more like a debugging visualiser - you may be able to leverage an existing tool for this.

(If you did want to do this in mostly XAML with standard ItemsControls and templates you might write a converter to expose properties of your ViewModel by reflection in some form that you can bind to, a collection of wrapper objects with exposed metadata, but I think ensuring that the properties exposed are properly bindable would be more work than it's worth)

like image 45
Nicholas W Avatar answered Sep 22 '22 00:09

Nicholas W


I'm halfway through implementing this now, I hope the following code will help anyone else trying to do this. It might be fun to turn into a more robust library.

AbstractView.xaml:

<UserControl x:Class="MyApplication.View.AbstractView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <StackPanel Name="container">
    </StackPanel>
</UserControl>

AbstractView.xaml.cs:

public partial class AbstractView : UserControl
{
    public AbstractView()
    {
        InitializeComponent();

        DataContextChanged += Changed;
    }

    void Changed(object sender, DependencyPropertyChangedEventArgs e)
    {
        object ob = e.NewValue;
        var props = ob.GetType().GetProperties();

        List<UIElement> uies = new List<UIElement>();
        foreach (var prop in props)
        {
            if (prop.PropertyType == typeof(String))
                uies.Add(makeStringProperty(prop));
            else if (prop.PropertyType == typeof(int))
                uies.Add(makeIntProperty(prop));
            else if (prop.PropertyType == typeof(bool))
                uies.Add(makeBoolProperty(prop));
            else if (prop.PropertyType == typeof(ICommand))
                uies.Add(makeCommandProperty(prop));
            else
            {
            }
        }

        StackPanel st = new StackPanel();
        st.Orientation = Orientation.Horizontal;
        st.HorizontalAlignment = HorizontalAlignment.Center;
        st.Margin = new Thickness(0, 20, 0, 0);
        foreach (var uie in uies) {
            if (uie is Button)
                st.Children.Add(uie);
            else
                container.Children.Add(uie);
        }
        if (st.Children.Count > 0)
            container.Children.Add(st);

    }

    UIElement makeCommandProperty(PropertyInfo prop)
    {
        var btn = new Button();
        btn.Content = prop.Name;

        var bn = new Binding(prop.Name);
        btn.SetBinding(Button.CommandProperty, bn);
        return btn;
    }

    UIElement makeBoolProperty(PropertyInfo prop)
    {
        CheckBox bx = new CheckBox();
        bx.SetBinding(CheckBox.IsCheckedProperty, getBinding(prop));
        if (!prop.CanWrite)
            bx.IsEnabled = false;
        return makeUniformGrid(bx, prop);
    }

    UIElement makeStringProperty(PropertyInfo prop)
    {
        TextBox bx = new TextBox();
        bx.SetBinding(TextBox.TextProperty, getBinding(prop));
        if (!prop.CanWrite)
            bx.IsEnabled = false;

        return makeUniformGrid(bx, prop);
    }

    UIElement makeIntProperty(PropertyInfo prop)
    {
        TextBlock bl = new TextBlock();
        bl.SetBinding(TextBlock.TextProperty, getBinding(prop));

        return makeUniformGrid(bl, prop);
    }

    UIElement makeUniformGrid(UIElement ctrl, PropertyInfo prop)
    {
        Label lb = new Label();
        lb.Content = prop.Name;

        UniformGrid u = new UniformGrid();
        u.Rows = 1;
        u.Columns = 2;
        u.Children.Add(lb);
        u.Children.Add(ctrl);

        return u;
    }

    Binding getBinding(PropertyInfo prop)
    {
        var bn = new Binding(prop.Name);
        if (prop.CanRead && prop.CanWrite)
            bn.Mode = BindingMode.TwoWay;
        else if (prop.CanRead)
            bn.Mode = BindingMode.OneWay;
        else if (prop.CanWrite)
            bn.Mode = BindingMode.OneWayToSource;
        return bn;
    }

}
like image 42
Oliver Avatar answered Sep 20 '22 00:09

Oliver


Pointer: Generate a dynamic DataTemplate as a string tied to the specific VM (Target). Parse it via XamlReader. Plonk it into your app resources in code.

Just an idea.. run with it.. Should be done by some type other than the View or the ViewModel.

like image 45
Gishu Avatar answered Sep 21 '22 00:09

Gishu