简体   繁体   中英

MVVM Dynamically add fields into View

I'm developing a WPF application using caliburn.micro MVVM framework.. In-order to develop a search screen, I need to dynamically load fields into the view, based on model properties.

Consider below view and view model:

  • SearchViewModel
  • SearchView

Let's assume T is a type of Product in below example.

public class SearchViewModel<T>
{
   public T Item{get;set;}
}

public class Product
{
   public int Id{get;set;}
   public string Name{get;set;}
   public string Description{get;set;}
}

I have a user control called SearchView.xaml with no contents on it. Whenever View is loaded new fields should be added to the view and field should be bound to the properties.

According to above code example, there are 3 public properties in the Product class, therefore 3 TextBoxes should be added to the view dynamically. When user enters data in the text field, corresponding property should be updated.

Is this possible? Can any experts help me to achieve this by providing some examples?

I would propose going about this differently. Instead of thinking about dynamically adding properties to a view / model, I would think about adding information about those properties to a list on the viewmodel. That list would then be bound to an ItemsControl with a template that looks like a TextBox .

So your view-model would have a property on it for the "thing" you want to examine. In the setter for this property, use reflection to enumerate the properties you are interested in, and add an instance of some kind of FieldInfo class (that you create) to the list of properties with the binding.

This has the benefit of keeping everything all MVVM compatible too, and there is no need to dynamically create controls with your own code.


The example below uses my own MVVM library (as a nuget package) rather than caliburn.micro, but it should be similar enough to follow the basic idea. The full source code of the example can be downloaded from this BitBucket repo .

As you can see in the included screenshots, the search fields are created dynamically on the view without any code in the view. Everything is done on the viewmodel. This also gives you easy access to the data that the user enters.

The view-model:

namespace DynamicViewExample
{
    class MainWindowVm : ViewModel
    {
        public MainWindowVm()
        {
            Fields = new ObservableCollection<SearchFieldInfo>();
            SearchableTypes = new ObservableCollection<Type>()
                              {
                                  typeof(Models.User),
                                  typeof(Models.Widget)
                              };

            SearchType = SearchableTypes.First();
        }

        public ObservableCollection<Type> SearchableTypes { get; }
        public ObservableCollection<SearchFieldInfo> Fields { get; }


        private Type _searchType;

        public Type SearchType
        {
            get { return _searchType; }
            set
            {
                _searchType = value;
                Fields.Clear();
                foreach (PropertyInfo prop in _searchType.GetProperties())
                {
                    var searchField = new SearchFieldInfo(prop.Name);
                    Fields.Add(searchField);
                }
            }
        }

        private ICommand _searchCommand;

        public ICommand SearchCommand
        {
            get { return _searchCommand ?? (_searchCommand = new SimpleCommand((obj) =>
            {
                WindowManager.ShowMessage(String.Join(", ", Fields.Select(f => $"{f.Name}: {f.Value}")));
            })); }
        }
    }
}

The SearchFieldInfo class:

namespace DynamicViewExample
{
    public class SearchFieldInfo
    {
        public SearchFieldInfo(string name)
        {
            Name = name;
        }

        public string Name { get; }

        public string Value { get; set; } = "";
    }
}

The view:

<Window
    x:Class="DynamicViewExample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:DynamicViewExample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="525"
    Height="350"
    d:DataContext="{d:DesignInstance local:MainWindowVm}"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ComboBox
            Grid.Row="0"
            ItemsSource="{Binding Path=SearchableTypes}"
            SelectedItem="{Binding Path=SearchType}" />
        <ItemsControl Grid.Row="1" ItemsSource="{Binding Path=Fields}">
            <ItemsControl.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding Path=Name}" />
                        <TextBox Width="300" Text="{Binding Path=Value}" />
                    </StackPanel>
                </DataTemplate>
            </ItemsControl.ItemTemplate>
        </ItemsControl>
        <Button Grid.Row="2" Command="{Binding Path=SearchCommand}">Search</Button>
    </Grid>
</Window>

The model classes:

class User
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string PhoneNumber { get; set; }
    public string Id { get; set; }
}

class Widget
{
    public string ModelNumber { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

搜索小部件模型 搜索用户模型

Here is a basic example of how you could generate a TextBox per public property of the T in the control using reflection.

SearchView.xaml:

<Window x:Class="WpfApplication4.SearchView"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApplication4"
        mc:Ignorable="d"
        Title="SearchView" Height="300" Width="300">
    <StackPanel x:Name="rootPanel">

    </StackPanel>
</Window>

SearchView.xaml.cs:

public partial class SearchView : UserControl
{
    public SearchView()
    {
        InitializeComponent();
        DataContextChanged += SearchView_DataContextChanged;
        DataContext = new SearchViewModel<Product>();
    }

    private void SearchView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (e.NewValue != null)
        {
            Type genericType = e.NewValue.GetType();
            //check the DataContext was set to a SearchViewModel<T>
            if (genericType.GetGenericTypeDefinition() == typeof(SearchViewModel<>))
            {
                //...and create a TextBox for each property of the type T
                Type type = genericType.GetGenericArguments()[0];
                var properties = type.GetProperties();
                foreach(var property in properties)
                {
                    TextBox textBox = new TextBox();
                    Binding binding = new Binding(property.Name);
                    if (!property.CanWrite)
                        binding.Mode = BindingMode.OneWay;
                    textBox.SetBinding(TextBox.TextProperty, binding);

                    rootPanel.Children.Add(textBox);
                }
            }
        }
    }
}

The other option will obviously be to create a "static" view for each type of T and define the TextBox elements in the XAML markup as usual.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM