How can I provide data templates dynamically from code?
I have view model MainWindowViewModel that I bind to MainWindow. It looks like this:
public class MainWindowViewModel : ViewModelBase
{
public int Progress
{
get => _progress;
set => RaiseAndSetIfChanged(ref _progress, value);
}
public ViewModelBase? Content
{
get => _content;
set => RaiseAndSetIfChanged(ref _content, value);
}
}
then in the MainWindow I chose DataTemplate based on a type tied to my MainWindowViewModel
<ContentControl Name="Header" Content="{Binding Content}">
<ContentControl.ContentTemplate>
<DataTemplate DataType="{x:Type viewModels:DefaultViewModel}">
<local:DefaultView></local:DefaultView>
</DataTemplate>
</ContentControl.ContentTemplate>
It is currently only a single DataTemplate but I would like to load it from different places so I'm trying to replace it with dynamically generated list of DataTemplates as in the future, some control I will need to load from external file provided by user.
How to achieve that?
You can create your own IDataTemplate implementation and forward calls to the actual template dynamically. E. g. ViewLocator from Avalonia MVVM template looks like this:
public class ViewLocator : IDataTemplate
{
public IControl Build(object data)
{
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
else
{
return new TextBlock { Text = "Not Found: " + name };
}
}
public bool Match(object data)
{
return data is ViewModelBase;
}
}
Related
I have a wpf application that I want to be able to launch a separate window in which I will have a listview bound to an observable collection. However I am unable to get the collection values to appear in the list view. Here is some of the code.
Window (Named WizardView):
(Data context defined like so at top of xaml):
d:DataContext="{d:DesignInstance Type=viewModels:MainViewModel}"
<Border Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" BorderBrush="Black">
<ListView BorderThickness="0" ItemsSource="{Binding TestModel.FailedTests}">
<Label Content="Introduction" FontWeight="Bold" />
<ListView.ItemTemplate>
<DataTemplate>
<StackPanel>
<Label Content="{Binding }"/>
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Border>
MainViewModel Code:
public class MainViewModel : BaseViewModel
{
public MainViewModel()
{
TestModel = new TestViewModel();
WizardModel = new WizardViewModel(TestModel);
}
private WizardViewModel _wizardModel;
public WizardViewModel WizardModel
{
get
{
return _wizardModel;
}
set
{
_wizardModel = value;
RaisePropertyChanged();
}
}
private TestViewModel _testViewModel;
public TestViewModel TestModel
{
get
{
return _testViewModel;
}
set
{
_testViewModel = value;
RaisePropertyChanged();
}
}
WizardViewModel Code:
public class WizardViewModel : TestViewModel
{
internal TestViewModel TestModel;
public WizardViewModel(TestViewModel testModel)
{
TestModel = testModel;
(TroubleShootCommand is defined in seperate UC, and launches fine)
TestModel.TroubleShootCommand = new DelegateCommand(Load, CanTroubleShoot);
}
public void Load()
{
(Sync Root is used because it is running on worker thread. Issue somewhere here?)
_syncRoot.Send(o =>
{
var troubleShootWizard = new WizardView();
troubleShootWizard.Owner = Application.Current.MainWindow;
troubleShootWizard.ShowDialog();
}, null);
}
Observable Collection in TestViewModel (Initialized in ctor):
private ObservableCollection<string> _failedTests;
public ObservableCollection<string> FailedTests
{
get { return _failedTests; }
set
{
_failedTests = value;
RaisePropertyChanged();
}
}
Any Help is appreciated, I feel like I have tried everything. I have watched values through the watch window under TestModel.FailedTests in the collection right before and right after launch.
First,
(Data context defined like so at top of xaml): d:DataContext="{d:DesignInstance Type=viewModels:MainViewModel}"
This is a mistake, this way d: you are defining the DataContext at design time..
You can create the viewmodel inside .xaml this way:
<WizardView.DataContext>
<viewModels:MainViewModel/>
</WizardView.DataContext>
Using the design time declaration can help in many ways like knowing the viewmodel in case you are creating it and assigning it in C# (or via a IoC mechanism), also it helps tools like IntelliSense and ReSharper to analyze your bindings and warn you if you misspell a property's name in xaml, auto-completion, etc... (more on this can be found here, and here)
Second, if you are assigning the WizardViewModel in your .xaml the same way (i.e. design-time), then you can either do it in your Load() function (add troubleShootWizard.DataContext = this;) or assign it in .xaml the same way I've mentioned before.
I have a MVVM application which has a WPF Grid which contains other embedded WPF Grids and at the same time, each of them contain some fields (WPF TextBlocks).
Very simplified example - View:
<Grid>
<Grid>
// Row definitions
// Colum definitions
<TextBlock Grid.Row="3" Grid.Column="0"
Text="{Binding Path=SomeField1}" />
<Grid>
<Grid>
// Row definitions
// Colum definitions
<TextBlock Grid.Row="0" Grid.Column="1"
Text="{Binding Path=SomeField2}" />
<Grid>
</Grid>
Each of these TextBlocks are bound to a string properties defined in view model.
View model (It implements INotifyPropertyChanged):
private string _someField1;
public string SomeField1
{
get return _someField1;
set
{
if (_someField1 == value) return;
_someField1 = value;
OnPropertyChanged("SomeField1");
}
}
private string _someField2;
public string SomeField2
{
get return _someField2;
set
{
if (_someField2 == value) return;
_someField2 = value;
OnPropertyChanged("SomeField2");
}
}
Then I have a model, I mean, a class with some public properties that is filled in by one process once data is obtained from a device. This class contains exactly the same properties as those defined in the view model.
Model:
public class MyModel
{
private string _someField1;
public string SomeField1
{
get return _someField1;
set
{
if (_someField1 == value) return;
_someField1 = value;
}
}
private string _someField2;
public string SomeField2
{
get return _someField2;
set
{
if (_someField2 == value) return;
_someField2 = value;
}
}
}
Later from view model I extract the data from this class (model), and I assign the values of those properties to the matching properties in view model. Finally, since view is bound to these properties, then view is correctly updated with values as below example.
View model method which extracts data received:
private void DataReceived(MyModel data)
{
this.SomeField1= data.SomeField1;
this.SomeField2= data.SomeField2;
}
The problem is that I have to define twice the properties, in view model and model. So I want to avoid this, I would like to bind Textblocks directly to properties in model and not defined the properties in view model to avoid redundant code. Or for example, is there any easy way to bind my model (MyModel) to the outer main grid and then textboxes bound to the properties in the view model (similar when bound itemsource in datagrid)?
I would suggest a generic view model:
public class BaseViewModel<TModel>
{
public TModel Model
{
get;
private set;
}
public BaseViewModel(TModel model)
{
this.Model = model;
}
}
Then you can bind to it:
<TextBlock Grid.Row="3" Grid.Column="0" Text="{Binding Path=Model.SomeField1}" />
I was thinking if below it is ok and respects MVVM pattern. I have thought it after seeing solution proposed by c0d3b34n. I think it is simpler and no need to do interfaces and generic view model. I have checked and it works:
Declare a property in view model:
private MyModel _model;
public MyModel Model
{
get { return _model; }
set
{
_model = value;
OnPropertyChanged("Model");
}
}
Then in the view:
<TextBlock Grid.Row="3" Grid.Column="0" Text="{Binding Path=Model.SomeField1}" />
... and the same for the rest of TextBlocks.
Finally:
private void DataReceived(MyModel data)
{
this.Model = data;
}
But as said by BionicCode in comments, this solution breaks MVVM pattern.
I'm woking on a project and I have three ViewModels: ObjectDetailsViewMode has a Context (property linking to a model) of type ObjectBase; PropertyTextViewModel has a Context of type PropertyText and PropertyNumberViewModel has a Context of type PropertyNumber.
Below is the structure of the Models:
public class ObjectBase : ModelBase
{
private string _name;
public string Name
{
get { return _name; }
set { SetProperty(ref _name, value); }
}
public DataCollection<PropertyBase> Properties { get; } = new DataCollection<PropertyBase>();
}
public class PropertyText : PropertyBase
{
private string _default;
public string Default
{
get { return _default; }
set { SetProperty(ref _default, value); }
}
}
public class PropertyNumber : PropertyBase
{
private double _default = 0;
public double Default
{
get { return _default; }
set { SetProperty(ref _default, value); }
}
private double _minValue = 0;
public double MinValue
{
get { return _minValue; }
set { SetProperty(ref _minValue, value); }
}
private double _maxValue = 0;
public double MaxValue
{
get { return _maxValue; }
set { SetProperty(ref _maxValue, value); }
}
}
Regarding the views I have one for each ViewModel. The ObjectDetailsView is a use control that has a TextBox for editing the Object.Name, two buttons to add new PropertyText/PropertyNumber to the Object.Properties and an ItemsControl connected to that Object.Properties.
Each PropertyBase in the ItemsControl (ItemsSource) is resolved into a new view using the DataTemplate marker:
<ItemsControl ItemsSource="{Binding Object.Properties}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type models:PropertyText}">
<views:PropertyTextView />
</DataTemplate>
<DataTemplate DataType="{x:Type models:PropertyNumber}">
<views:PropertyNumberView />
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
As I'm using PRISM the correct ViewModel is automatically created for me and the view DataContext is then set to the new ViewModel. My problem is I need to pass the new Property from the Object.Properties list to the newly created View's ViewModel and store it in the Context property I have there.
I can't avoid creating a View/ViewModel for each property type because there is some under-the-hood logic on some Property types (not the ones I described here.. but I have other types like Boolean, Reference, Enum...)
So I really need to pass a value to the ViewModel I tried to use
<ItemsControl ItemsSource="{Binding Object.Properties}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type models:PropertyText}">
<views:PropertyTextView Context="{Binding}"/>
</DataTemplate>
<DataTemplate DataType="{x:Type models:PropertyNumber}">
<views:PropertyNumberView Context="{Binding}"/>
</DataTemplate>
</ItemsControl.Resources>
</ItemsControl>
Be aware that Context is a custom property I created inside the ViewModel's to store the ModelContext. I even created a DependencyProperty in the View's behind code:
public PropertyBase Context
{
get { return (PropertyBase)GetValue(ContextProperty); }
set { SetValue(ContextProperty, value); }
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ContextProperty =
DependencyProperty.Register("Context", typeof(PropertyBase), typeof(PropertyTextView), new PropertyMetadata(null));
But it doesn't get linked to the ViewModels set event (I made a break point there and... nothing). I even tried a SetBinding in the PropertyTextView code-behind (constructor):
string propertyInViewModel = "Context";
var bindingViewMode = new Binding(propertyInViewModel) { Mode = BindingMode.TwoWay };
this.SetBinding(ContextProperty, bindingViewMode);
No luck with any of these... I' really stuck.
Something More Simple
If the PropertyTextView has this dependency property.
public string Context
{
get { return (PropertyBase)GetValue(ContextProperty); }
set { SetValue(ContextProperty, value); }
}
// Using a DependencyProperty as the backing store for Context. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ContextProperty =
DependencyProperty.Register("Context", typeof(string), typeof(PropertyTextBuilderView), new PropertyMetadata(null));
I should be able to do:
right?! Why isn't the public property "Context" not being called (I placed a breakpoint there and I get nothing).
Instead of just setting the Context Property of your View to a new Binding you need to assign the Current DataContext like so:
<views:PropertyNumberView Context="{Binding .}"/>
This should assign the Current Views.DataContext Property to your new View.
If you're in an DataTemplate you probably need to specify the RelativeSource:
<views:PropertyNumberView Context="{Binding Path=DataContext, RelativeSource={RelativeSource AncestorType=UserControl}}
<ItemsControl ItemsSource="{Binding Object.Properties}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type models:PropertyText}">
<views:PropertyTextView Context="{Binding .}"/>
</DataTemplate>
<ItemsControl.Resources>
</ItemsControl>
As I'm using PRISM the correct ViewModel is automatically created for me
You don't have to use view-first with Prism. The ViewModelLocator is there to help, if you chose to, but view model-first is possible, too.
If I understand you correctly, you have a view model and want to populate a list with child view models. So do just that:
internal class ParentViewModel : BindableBase
{
public ParentViewModel( ParentModel parentModel, IChildViewModelFactory factory )
{
Children = new object[] { factory.CreateTextViewModel(parentModel.TextProperty), factory.CreateNumberViewModel(parentModel.NumberProperty) };
}
public IEnumerable Children { get; }
}
and map the different child view models to child views via DataTemplates.
parentModel.WhateverProperty will have a Name and Value properties as well as setter for the value, probably...
I have a panel and my idea is to have it populated by a stack panel containing two text boxes. When the user enters something in the left box, something should be generated in the right one, as follows.
<StackPanel Orientation="Horizontal">
<TextBox Name="Lefty" LostFocus="FillMyBuddy" />
<TextBox Name="Righty" LostFocus="FillMyBuddy" />
</StackPanel>
However, I'd like to add an option to add/remove rows and, since I wish not to limit myself to the number of such, I get a bit uncertain regarding the approach on two points.
Manipulating DOM (well, it's XAML/WPF but you see what I'm aiming at).
Event handling.
Is it a big no-no to programmatically affect the mark-up structure of the window? Or is it OK to add/remove panels during run-time?
What would the recommended way to be if I want the Lefty number 3 change stuff in Righty number 3? Anything more neat than checking the sender and pulling its siblings from the parent? I want to use a single event handler for any and all rows (knowing that the operations are always intra-row-wise).
You will want to follow MVVM, and have no code in your code-behind (programmatically affect the mark-up structure) files. The concept is easy when you grasp it, so learn it before you start writing your code.
In short, you are going to want to have a view model (something that implements INotifyPropertyChanged (INPC)) which holds your collection of items (which are going to be models, or view models in pure-MVVM). In "hybrid"-MVVM you could just have your models implement INPC.
Then, through the use of commands, you'd implement the logic to remove items from the list that its in. You can pass references, raise notification, using event bubbling, etc. (it's your preference) to have the item actually removed. In my case, I just passed a "manager" to the hybrid-model and held a reference to that. When the command is called (button is clicked), the model calls for the reference to remove itself from the list.
After you do that you define a DataTemplate to define what an "item" should look like one the View. You use a ItemsControl to show a collection of items, and bind to its ItemsSource so the collection of items are shown. Set your ItemsControl.ItemTemplate to the DataTemplate you created, and anything added to the collection bound to ItemsSource of the type defined in DataTemplate.DataType will render as you specify in the DataTemplate.
At the end of the day, you should learn about MVVM design, DataContext, INPC, Commands, Control types and their "main" properties, e.g. everything that inherits from ItemsControl has an ItemsSource property.
Here is a working example, where changing the original string, will reverse it and put it in the read-only right side text box:
MainWindow.xaml.cs (code-behind)
public partial class MainWindow : Window
{
StructureVm _struct = new StructureVm("Test");
public MainWindow()
{
InitializeComponent();
DataContext = _struct;
}
}
MainWindow.xaml (View)
<Window x:Class="DataTemplateWithCommands.MainWindow"
xmlns:local="clr-namespace:DataTemplateWithCommands"
Title="MainWindow" Height="350" Width="525" Background="Orange">
<Window.Resources>
<DataTemplate DataType="{x:Type local:Model}"
x:Key="VmItem">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Original, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Text="{Binding Encoded}"
IsReadOnly="True" />
<Button Content="X"
Command="{Binding RemoveMeCommand}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="{Binding Items}"
ItemTemplate="{StaticResource VmItem}">
</ItemsControl>
</Grid>
</Window>
Interface (helpful for Dependency Injection)
public interface IStructureManager
{
bool RemoveItem(Model itemToRemove);
}
ViewModel
public class StructureVm : IStructureManager
{
private readonly ObservableCollection<Model> _items;
private readonly string _title;
public StructureVm(string title)
{
_title = title;
_items = new ObservableCollection<Model>
{
new Model(this, "12"),
new Model(this, "23"),
new Model(this, "34"),
new Model(this, "45"),
new Model(this, "56"),
new Model(this, "67"),
new Model(this, "78"),
new Model(this, "89"),
};
}}
public ObservableCollection<Model> Items
{
get
{
return _items;
}
}
public string Title
{
get
{
return _title;
}
}
public bool RemoveItem(Model itemToRemove)
{
return _items.Remove(itemToRemove);
}
}
Model (not pure-MVVM, pure MVVM models don't implement INPC, and don't have Command in them)
public class Model : INotifyPropertyChanged
{
private readonly RelayCommand _removeMe;
private string _original;
private string _encoded;
private readonly IStructureManager _manager;
public string Original
{
get
{
return _original;
}
set
{
_original = value;
Encoded = ReverseString(_original);
NotifyPropertyChanged();
}
}
public string Encoded
{
get
{
return _encoded;
}
set
{
_encoded = value;
NotifyPropertyChanged();
}
}
public ICommand RemoveMeCommand
{
get
{
return _removeMe;
}
}
public Model(IStructureManager manager, string original)
{
Original = original;
_manager = manager;
_removeMe = new RelayCommand(param => RemoveMe(), param => CanRemoveMe);
}
private void RemoveMe()
{
_manager.RemoveItem(this);
}
private bool CanRemoveMe
{
get
{
//Logic to enable/disable button
return true;
}
}
private string ReverseString(string s)
{
char[] arr = s.ToCharArray();
Array.Reverse(arr);
return new string(arr);
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName]string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
RelayCommand implementation
From here on out all you have to do is change the attributes of your controls to whatever you're happy with and call it good. The example might be ugly, but I'm leaving it as an exercise for you to figure out other properties/attributes of WPF controls.
i use MVVM to built my project, now i have some troubles,when i click a button, i want get data from view to viewmodel, what should i do?
thanks
Bob
Bind that data to the view model and execute a command when the user clicks the button. The command and data are housed in the view model, so it has everything it needs.
public class YourViewModel : ViewModel
{
private readonly ICommand doSomethingCommand;
private string data;
public YourViewModel()
{
this.doSomethingCommand = new DelegateCommand(this.DoSomethingWithData);
}
public ICommand DoSomethingCommand
{
get { return this.doSomethingCommand; }
}
public string Data
{
get { return this.data; }
set
{
if (this.data != value)
{
this.data = value;
this.OnPropertyChanged(() => this.Data);
}
}
}
private void DoSomethingWithData(object state)
{
// do something with data here
}
}
XAML:
<TextBox Text="{Binding Data}"/>
<Button Command="{Binding DoSomethingWithData}"/>
For information on the various dependencies in the above example such as ViewModel and DelegateCommand, see my series of posts on MVVM.
EDIT after receiving more info: For tracking item selection, simply introduce a view model to represent the item:
public class CustomerViewModel : ViewModel
{
private bool isSelected;
public bool IsSelected
{
get { return this.isSelected; }
set
{
if (this.isSelected != value)
{
this.isSelected = value;
this.OnPropertyChanged(() => this.IsSelected);
}
}
}
}
Your "main" view model would expose a collection of these items (generally an ObservableCollection<T>):
public ICollection<CustomerViewModel> Customers
{
get { return this.customers; }
}
Your view would then bind as:
<ListBox ItemsSource="{Binding Customers}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsSelected" Value="{Binding IsSelected}"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
Notice how each ListBoxItem will have its IsSelected property bound to the CustomerViewModel.IsSelected property. Thus, your main view model can just check this property to determine which customers are selected:
var selectedCustomers = this.Customers.Where(x => x.IsSelected);
The solution suggested by Kent is in my opinion by far the best/only one to follow MVVM.
If however you don't want to replicate/reflect listbox selections to the view model or you want a quick and - according to MVVM - dirty solution, you can use the command parameter to send data from the view to the view model.
For that you have to bind the CommandParameter property of the button to the property which contains the data you want to send to the view model. For simplicity I just used a TextBox.
<StackPanel Orientation="Vertical">
<TextBox x:Name="Data"/>
<Button Content="DoSomething"
Command="{Binding Path=DoSomethingCommand}"
CommandParameter="{Binding ElementName=Data, Path=Text}"/>
</StackPanel>
The ViewModel of the sample looks like the following.
public class ViewModel
{
private ICommand doSomethingCommand = new MyCommand();
public ICommand DoSomethingCommand
{
get
{
return doSomethingCommand;
}
}
}
With this, you will get the specified content as the parameter in the Execute method of ICommand.
public class MyCommand : ICommand
{
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public void Execute(object parameter)
{
string dataFromView = (string)parameter;
// ...
MessageBox.Show(dataFromView);
}
}