Passing ModelData (context) between UserControls (views) MVVM Prism - c#

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...

Related

ListBox emtpy when bound to populated ICollectionView

I'm trying to implement filtering on a UserControl (which is essentially just a ListBox with a data template) using ICollectionView.
When I bind to the ICollectionView my LOAListBox is empty.
My xaml looks like this:
<TextBox Text="{Binding SearchString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<!-- LOA_List is a DependencyProperty which binds to ListBox.ItemsSource -->
<controls:LOAListBox LOA_List="{Binding FilteredView, Mode=OneWay}" />
And in my view model, I do this:
public class LOAViewModel : ViewModelBase
{
public ICollectionView FilteredView { get; private set; }
private string _searchString;
public string SearchString
{
get => _searchString;
set
{
_searchString = value;
RaisePropertyChanged("SearchString");
FilteredView.Refresh();
}
}
private List<LOA> _available_LOAs;
public List<LOA> Available_LOAs
{
get => _available_LOAs;
set
{
_available_LOAs = value;
RaisePropertyChanged("Available_LOAs");
}
}
public LOAViewModel()
{
Available_LOAs = data.GetLOAData();
FilteredView = CollectionViewSource.GetDefaultView(Available_LOAs);
FilteredView.Filter = new Predicate<object>(o => Filter(o as LOA));
}
private bool Filter(LOA loa)
{
return SearchString == null || loa.Display_Name.Contains(SearchString);
}
}
During debugging I can see that Available_LOAs is not empty and after GetDefaultView FilteredView also has that same collection. There aren't any binding errors. I also made by filter method always return true just to remove that possibility.
I feel like I must be missing a step but I've checked various other online examples and I can't find anything... My hunch is that it's related to the fact I'm binding to a ListBox nested in a UserControl, but I don't understand why that would matter when it works if change the binding from FilteredView to Available_LOAs directly.
Update; this is the simplified code for LOAListBox:
XAML:
<UserControl>
<ListBox ItemsSource="{Binding LOA_List, Mode=OneWay, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type UserControl}}}"/>
</UserControl>
Code-behind:
public partial class LOAListBox : UserControl
{
public static readonly DependencyProperty DataSource = DependencyProperty.Register(nameof(LOA_List), typeof(List<LOA>), typeof(LOAListBox), new PropertyMetadata());
public List<LOA> LOA_List
{
get => (List<LOA>)GetValue(DataSource);
set => SetValue(DataSource, value);
}
}
You cannot bind an ICollectionView to a List<T> property.
Change the type of your dependency property to IEnumerable:
public static readonly DependencyProperty DataSource = DependencyProperty.Register(nameof(LOA_List),
typeof(IEnumerable), typeof(LOAListBox), new PropertyMetadata());
public IEnumerable LOA_List
{
get => (IEnumerable)GetValue(DataSource);
set => SetValue(DataSource, value);
}
As a side note, you should also change the name of the dependency property from "DataSource" to "LOA_ListProperty" (and remove the underscore from both names) to follow the naming convention.
After going step-by-step to reproduce the issue, I eventually realised that I wasn't notifying of changes to FilteredView and, not helping matters, I was changing the ICollectionView source without reassigning the ICollectionView,
So I made my FilteredView a standard property that calls RaisePropertyChanged():
private ICollectionView _filteredView;
public ICollectionView FilteredView
{
get => _filteredView;
set
{
_filteredView = value;
RaisePropertyChanged("FilteredView");
}
}
And when I change the ICollectionView source variable I reassign based on the new source collection:
FilteredView = CollectionViewSource.GetDefaultView(Available_Destination_LOAs);
FilteredView.Filter = new Predicate<object>(o => Filter(o as LOA));

WPF Binding to Custom Property Not Working

I have this control to display a list of usercontrols
<ItemsControl x:Name="LayersList" Margin="10,284,124,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<NaturalGroundingPlayer:LayerControl Item="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
The LayerControl control contains this code
public partial class LayerControl : UserControl {
public LayerItem Item { get; set; }
public static readonly DependencyProperty ItemProperty =
DependencyProperty.Register(
"Item",
typeof(LayerItem),
typeof(LayerControl),
new PropertyMetadata(null));
public LayerControl() {
InitializeComponent();
}
private void Button_Click(object sender, RoutedEventArgs e) {
// This doesn't work because Item remains null
MainWindow.Instance.LayersList.Items.Remove(Item);
}
}
LayerItem contains this
[PropertyChanged.ImplementPropertyChanged]
public class LayerItem {
public LayerType Type { get; set; }
public string FileName { get; set; }
}
public enum LayerType {
Audio,
Video,
Image
}
Problem is: The Binding is setting the Item property to null. If I change the binding to {Binding Type} instead of {Binding} (and adapt the property type accordingly), then it works. But I can't find a way to bind the whole object. What am I doing wrong?
On a side note, I tried setting ItemsControl.ItemsSource to a ObservableCollection<LayerItem> but that didn't seem to work. Adding items directly to ItemsControl.Items is working. Any idea why that is?
You have incorrectly implemented a dependency property. You should use GetValue and SetValue methods instead of creating an auto-property.
public static readonly DependencyProperty ItemProperty =
DependencyProperty.Register(
"Item", typeof(LayerItem), typeof(LayerControl));
public LayerItem Item
{
get { return (LayerItem)GetValue(ItemProperty); }
set { SetValue(ItemProperty, value); }
}
P.S. You shouldn't access controls like this: MainWindow.Instance.LayersList.Items.Remove(Item). You should use MVVM instead. I'm also not convinced this property is required at all. DataContext may be enough.

Use a binding to set the text property of a textbox in a usercontrol - WPF

I have a user control which contains a textbox and have created a get/set in the usercontrol to get/set the text property of the textbox.
public class OpenFileControl : UserControl
{
StackPanel sp;
public TextBox tb;
public string Text { get { return tb.Text; } set { tb.Text = value; } }
I then want to set this value based on a binding later on -
<gX3UserControls:OpenFileControl Text="{Binding Value}" />
But I get the following exception
A 'Binding' cannot be set on the 'Text' property of type 'OpenFileControl'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject.
After some investigation It seems Text needs to be a dependency property, but If I do that I cant work out how to pass the value on to the textbox.
How can I fix this.
Consider using something like this.
Control XAML:
<UserControl x:Class="WpfTestBench.OpenFileControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel>
<TextBox Text="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}},
Path=Filename, UpdateSourceTrigger=PropertyChanged}" />
</StackPanel>
</UserControl>
Control codebehind:
using System.Windows;
namespace WpfTestBench
{
public partial class OpenFileControl
{
public static readonly DependencyProperty FilenameProperty =
DependencyProperty.Register("Filename", typeof (string), typeof (OpenFileControl));
public OpenFileControl()
{
InitializeComponent();
}
public string Filename
{
get { return (string)GetValue(FilenameProperty); }
set { SetValue(FilenameProperty, value); }
}
}
}
Main XAML:
<Window x:Class="WpfTestBench.OpenFileWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfTestBench="clr-namespace:WpfTestBench"
Title="OpenFileWindow" Width="300" SizeToContent="Height">
<StackPanel>
<wpfTestBench:OpenFileControl x:Name="In" Filename="{Binding SelectedFilename, UpdateSourceTrigger=PropertyChanged}" />
<wpfTestBench:OpenFileControl x:Name="Out" Filename="{Binding ElementName=In, Path=Filename}" />
</StackPanel>
</Window>
Main codebehind:
namespace WpfTestBench
{
public partial class OpenFileWindow
{
public OpenFileWindow()
{
InitializeComponent();
DataContext = this;
}
public string SelectedFilename { get; set; }
}
}
Execution result (after typing something in the first control):
If you define the dependency property as the static and the actual property, you can write whatever code behind you want in the body of the property.
public const string TextPropertyName = "Text";
public string Text
{
get
{
return (string)GetValue(TextProperty);
}
set
{
SetValue(TextProperty, value);
}
}
public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
TextPropertyName,
typeof(string),
typeof(MyControl),
new UIPropertyMetadata(false));
In the getter and setter you can do something like textBox1.Text = value; but you'd probably be better served using a binding to the property instead. MVVM frameworks make light work of this sort of thing quite often. You might find more success defining a ViewModel (a class with an appropriate FielPath variable for example) and setting the DataContext of the new UserControl to be an instance of the ViewModel class, using Bindings to do the heavy lifting for you.

MVVM binding view to a changeable model

Lets say i have a ListBox with multiple model objects, lets call it person with the normal values like name, age, familiyName and a list of objects professions with arbitrary values.
I want to create an Inspector control now, which shows the values of the selected person object but my questing is.
Should the Inspector viewmodel have its own properties (like SelectedPersonName, SelectedPersonAge etc..) to bind the view to, and update all of them in the event that the selected item in the listbox changes? Or should i implement it by simply having a reference in the inspector viewmodel referencing the selected item in the listbox, ending up with bindings like {Binding SelectedPerson.name} {Binding SelectedPerson.age} What is best practice here? is there a third way?
The second way. As long as you notify when the selected person changes, and when the properties on that person change, it will all bind correctly. And you won't need to make a bunch of new viewmodel properties.
you can use one viewModel for each model and initialize them in their constructors like this:
public class PersonVm : DependencyObject
{
public PersonVm(Model.Person model)
{
_model = model;
Name = model.Name;
Age = model.Age;
foreach (var professionModel in model.Professions)
{
Professions.Add(new ProfessionVm(professionModel));
}
}
Model.Person _model;
public int Id { get { return _model.Id; } }
//Name Dependency Property
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register("Name", typeof(string), typeof(PersonVm), new UIPropertyMetadata(null));
//Age Dependency Property
public int Age
{
get { return (int)GetValue(AgeProperty); }
set { SetValue(AgeProperty, value); }
}
public static readonly DependencyProperty AgeProperty =
DependencyProperty.Register("Age", typeof(int), typeof(PersonVm), new UIPropertyMetadata(0));
//Professions Observable Collection
private ObservableCollection<ProfessionVm> _professions = new ObservableCollection<ProfessionVm>();
public ObservableCollection<ProfessionVm> Professions { get { return _professions; } }
}
This is the viewModel for the whole page or window, you need to create one instance of it in the constructor of the page or window and set DataContext to it after InitializeComponent()
public class MainViewModel : DependencyObject
{
public MainViewModel(IEnumerable<Model.Person> models)
{
foreach (var personModel in models)
{
People.Add(new PersonVm(personModel));
}
}
//People Observable Collection
private ObservableCollection<PersonVm> _people = new ObservableCollection<PersonVm>();
public ObservableCollection<PersonVm> People { get { return _people; } }
//SelectedPerson Dependency Property
public PersonVm SelectedPerson
{
get { return (PersonVm)GetValue(SelectedPersonProperty); }
set { SetValue(SelectedPersonProperty, value); }
}
public static readonly DependencyProperty SelectedPersonProperty =
DependencyProperty.Register("SelectedPerson", typeof(PersonVm), typeof(MainViewModel), new UIPropertyMetadata(null));
}
This way you can bind very easily like this:
<DockPanel>
<ListBox
DockPanel.Dock="Left"
ItemsSource="{Binding People}"
SelectedItem="{Binding SelectedPerson}"
DisplayMemberPath="Name"/>
<StackPanel DataContext="{Binding SelectedPerson}">
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}"/>
<ListBox ItemsSource="{Binding Professions}"
DisplayMemberPath="Whatever"/>
</StackPanel>
</DockPanel>
The benefit of this method is easy Binding. Also the updating of the model is done in the following way:
//Name Dependency Property
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register("Name", typeof(string), typeof(PersonVm),
new UIPropertyMetadata(null, (d, e) =>
{
var vm = (PersonVm)d;
var val = (string)e.NewValue;
vm._model.Name = val;
}));

Binding User Control Property with another Property

In my Windows Store Application I want to bind a Property in a user control with another Property in logical class
the User Control "Card_UC.xaml.cs" contains this Property:
public string Card_ID
{
get { return (string)GetValue(Card_ID_Property); }
set { SetValue(Card_ID_Property, value); }
}
public DependencyProperty Card_ID_Property =
DependencyProperty.Register(
"Card_ID",
typeof(string),
typeof(Card_UC),
new PropertyMetadata(null));
and in my logical class "Card_Data.cs":
public string Card_ID { get; set; }
In Main Page I want to make a Grid of this Cards using data binding like this
<GridView
x:Name="UI_GView_Cards"
ItemsSource="{Binding}">
<GridView.ItemTemplate>
<DataTemplate>
<local:CardControl
x:Name="UC_Card"
CardPressed="CardControl_CardPressed"
ID="{Binding Path=Card_ID, ElementName=Card_UC, Mode=TwoWay}"/>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
all the other Properties binding in the "Card_UC.xaml" working except Card_ID
the problem now is that the application crashes every time I access the ID Property using
return (string)GetValue(Card_ID_Property);
Error: "Object reference not set to an instance of an object."
Problem Fixed:
the problem in this line:
ID="{Binding Path=Card_ID, ElementName=Card_UC, Mode=TwoWay}"
changed to:
ID="{Binding Card_ID}"
Edit:
Fixed "Copy/Paste" mistake.
Re-Format the question.
It looks like you have a cut and paste error:
public string Card_ID
{
get { return (string)GetValue(UCAppsProperty); }
set { SetValue(UCAppsProperty, value); }
}
to:
public string Card_ID
{
get { return (string)GetValue(Card_ID_Property); }
set { SetValue(Card_ID_Property, value); }
}

Categories