I have a custom user control, and it has a dependency property. That custom user control is kind of complicated so I decided to make a view model for it, but I haven't implemented it yet. I'm thinking of making the view model having some properties which are bound to the custom user control.
Here is my code sample,
UserControl.xaml
<StackPanel>
<TextBlock Text={Binding Age} />
<TextBlock Text={Binding Name} />
</StackPanel>
UserControl.cs
public Person Person
{
get { return (Person)GetValue(PersonProperty); }
set { SetValue(PersonProperty, value); }
}
public static readonly DependencyProperty PersonProperty = DependencyProperty.Register("Person", typeof(Person), typeof(SampleUserControl), new PropertyMetadata(null, propertyChangedCallback));
private static void propertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// I want to update the view model here
// Something like the following
(this.DataContext as MyViewModel).Person = Person;
}
MyViewModel
public Person Person
{
get { return _person; }
set
{
_pserson = person;
RaisePorpertyChanged("Age");
RaisePorpertyChanged("Name");
}
}
public int Age{ get; set; }
public string Name{ get; set; }
So, do you think it's a good practice? I mean updating a view model when a dependency property gets updated, and hopefully someone teaches me how to update the view model inside the PropertyChangedCallback :) BTW I'm using the MVVM Light toolkit.
According to this question, http://forums.silverlight.net/forums/p/133665/298671.aspx, I can't use a view model as the DataContext of a user control to bind some UI in a usual way.
Related
So i have this ViewModel class:
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<Person> _persons;
public ObservableCollection<Person> Porsons
{
get { return _persons; }
set
{
_persons = value;
NotifyPropertyChanged();
}
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
And then create this ViewModel class and populate its Person list:
ViewModel viewModel;
ObservableCollection<Person> persons
public MainWindow()
{
InitializeComponent();
viewModel = new ViewModel();
viewModel.Porsons= persons;
}
And then my ListView:
<ListView ItemSource={Binding Persons}/>
So instead of binding this Persons list into my ViewModel class and then do this ItemSource can i do it in pure XAML or this is the right way ?
Instead of creating a ViewModel property on your view it is recommended to use it's DataContext (this link also shows how to set it using XAML). Also don't populate the view model in the view since most of the time the data resides in the model and the view should not know anything about any models (when following MVVM).
Please read the link above and visit the links you meet. Also read this article about MVVM. This gives you some basic knowledge to make it easier to understand how to use the WPF framework.
There are many variations of view model creation in XAML.
For example alternatively you can create it in the App.Xaml to make it globally accessible via the StaticResource markup extension and assign it to the individual controls's DataContext via a Style or use an ObjectDataProvider.
This example uses XAML Property Element declaration to create a ViewModel instance directly in the target view. This instance is locally accessible only.
ViewModel.cs:
namespace Example
{
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
this.Persons = new ObservableCollection<Person>();
}
private ObservableCollection<Person> _persons;
public ObservableCollection<Person> Persons
{
get => _persons;
set
{
_persons = value;
NotifyPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
View.xaml:
<Window x:Class="Example.MainWindow"
...
xmlns:local="clr-namespace:Example">
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Grid>
<ListView ItemSource={Binding Persons}/>
</Grid>
</Window>
Yes, you can. But no, you most certainly do not want to.
To answer your question, let's say your Person class looks like this:
public class Person
{
public string Name { get; set; }
}
You can easily declare a list in XAML and bind it to a ListView (say) like this:
<ListView DisplayMemberPath="Name">
<ListView.ItemsSource>
<x:Array Type="{x:Type vm:Person}">
<vm:Person Name="Tom" />
<vm:Person Name="Dick" />
<vm:Person Name="Harry" />
</x:Array>
</ListView.ItemsSource>
</ListView>
The result of which is this:
Just because you can do this, though, doesn't mean you should. The whole point of MVVM is to separate your view layer from your view model layer. You should be able to run your entire application from a test build without creating a single view object at all. In asking this question what you are apparently trying to do is declare a data structure in your view layer, which is totally the wrong place to put it. Your view layer should be as "dumb" as possible, with only the weakest possible bindings to your view model layer where the actual logic is going on.
I am trying to create a composite DataContext for a UserControl. Basically I have a control which has Order and Package properties and I wanted to create the composite object representing this datasource in XAML rather than in code.
This is how I am trying to display the UserControl (and create the DataContext):
<views:PackageDetailsControl>
<views:PackageDetailsControl.DataContext>
<vm:OrderPackagePair Package="{Binding Package, Mode=OneWay}"
Order="{Binding Order, Mode=OneWay}"/>
</views:PackageDetailsControl.DataContext>
</views:PackageDetailsControl>
The OrderPackagePair object is a simple dependency object that is created in XAML :
public class OrderPackagePair : DependencyObject
{
public OrderDetails Order
{
get { return (OrderDetails)GetValue(OrderProperty); }
set { SetValue(OrderProperty, value); }
}
public static readonly DependencyProperty OrderProperty =
DependencyProperty.Register("Order", typeof(OrderDetails), typeof(OrderPackagePair), new UIPropertyMetadata(null));
public PackageInfo Package
{
get { return (PackageInfo)GetValue(PackageProperty); }
set { SetValue(PackageProperty, value); }
}
public static readonly DependencyProperty PackageProperty =
DependencyProperty.Register("Package", typeof(PackageInfo), typeof(OrderPackagePair), new UIPropertyMetadata(null));
}
Order and Package are not bound correctly and are just null.
Yes I know there's probably a better way of doing this - but I cannot understand why this isn't working. Occasionally in Blend it'll work and then go blank again.
This will not work because DependencyObject(OrderPackagePair class) doesn't monitor internal changes of its dependency properties. As OrderPackagePair object remains the same, DataContext considered as unchanged.
On the opposite site, class Freezable is intented to notify subscribers that instance was changed when one of its dependency properties changed.
So, try to declare Freezable instead of DependencyObject as base class of OrderPackagePair.
------------- UPDATE --------
Yes, it works. In order to prove it I've implemented simple example.
Code of OrderPackagePairClass:
public class OrderPackagePair : Freezable
{
public OrderDetails Order
{
get { return (OrderDetails)GetValue(OrderProperty); }
set { SetValue(OrderProperty, value); }
}
public static readonly DependencyProperty OrderProperty =
DependencyProperty.Register("Order", typeof(OrderDetails), typeof(OrderPackagePair), new UIPropertyMetadata(null));
public PackageInfo Package
{
get { return (PackageInfo)GetValue(PackageProperty); }
set { SetValue(PackageProperty, value); }
}
public static readonly DependencyProperty PackageProperty =
DependencyProperty.Register("Package", typeof(PackageInfo), typeof(OrderPackagePair), new UIPropertyMetadata(null));
protected override Freezable CreateInstanceCore()
{
throw new NotImplementedException();
}
}
XAML:
<Window x:Class="WindowTest.MainWindow"
xmlns:self="clr-namespace:WindowTest"
Name="RootControl">
<StackPanel Margin="10" DataContextChanged="StackPanel_DataContextChanged">
<StackPanel.DataContext>
<self:OrderPackagePair Package="{Binding Path=DataContext.PackageInfo, Mode=OneWay, ElementName=RootControl}"
Order="{Binding Path=DataContext.OrderDetails, Mode=OneWay, ElementName=RootControl}"/>
</StackPanel.DataContext>
<Button Margin="10" Content="Change Package" Click="Button_Click"/>
</StackPanel>
</Window>
And code behind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
}
private OrderDetails _orderDetails;
public OrderDetails OrderDetails
{
get
{
return this._orderDetails;
}
set
{
this._orderDetails = value;
this.OnPropertyChanged("OrderDetails");
}
}
private PackageInfo _packageInfo;
public PackageInfo PackageInfo
{
get
{
return this._packageInfo;
}
set
{
this._packageInfo = value;
this.OnPropertyChanged("PackageInfo");
}
}
private void Button_Click(object sender, RoutedEventArgs e)
{
this.PackageInfo = new PackageInfo(DateTime.Now.ToString());
}
private void StackPanel_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
Trace.WriteLine("StackPanel.DataContext changed");
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
var safeEvent = this.PropertyChanged;
if (safeEvent != null)
{
safeEvent(this, new PropertyChangedEventArgs(name));
}
}
}
When you click the button, model changes PackageInfo property (for simplicity model and view are implemented in the same class). Dependency property OrderPackagePair.Package reacts on new value and overwrites its value. Due to Freezable nature, OrderPackagePair notifies all subscribers that it was changed and handler StackPanel_DataContextChanged is called. If you get back to DependencyObject as base class of OrderPackagePair - handler will be never called.
So, I suppose your code doesn't work because of other mistakes. You should carefully work with DataContext. For example, you wrote:
<views:PackageDetailsControl>
<views:PackageDetailsControl.DataContext>
<vm:OrderPackagePair Package="{Binding Package, Mode=OneWay}"
Order="{Binding Order, Mode=OneWay}"/>
</views:PackageDetailsControl.DataContext>
</views:PackageDetailsControl>
and certainly this is one of the problems. Binding expression is oriented on current DataContext. But you set DataContext as OrderPackagePair instance. So you binded OrderPackagePair.Package to OrderPackagePair.Package (I suppose, that your goal is to bind OrderPackagePair.Package to Model.Package). And that's why nothing happened.
In my example in binding expression I explicitly tell to which DataContext I want to bind:
Package="{Binding Path=DataContext.PackageInfo, Mode=OneWay, ElementName=RootControl}"
Let me explain my situation. I have made a user control that contains an ItemsControl
<ItemsControl Name="itemControlReviewTags">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel>
<my:ReviewControl ReviewEvent="{Binding}" />
<TextBlock Text="{Binding Text}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
This ItemsControl is bound to an observablecollection in the code behind
public ObservableCollection<TagEvent> tagItems = new ObservableCollection<TagEvent>();
The collection is set on the ItemsControl like so
itemControlReviewTags.ItemsSource = tagItems;
The TagEvent class is defined like below. The class is added to the collection at certain events.
public class TagEvent : EventArgs
{
public string Text { get; set; }
public string Comment { get; set; }
public string Value { get; set; }
public DateTime Time { get; set; }
public string Type { get; set; }
}
The ReviewControl in the datatemplate had a DependencyProperty like so
public TagEvent ReviewEvent
{
get
{
return (TagEvent)GetValue(ReviewEventProperty);
}
set
{
SetValue(ReviewEventProperty, value);
}
}
public static readonly DependencyProperty ReviewEventProperty = DependencyProperty.Register("ReviewEvent", typeof(TagEvent), typeof(ReviewControl), new PropertyMetadata(new TagEvent() { Comment = "hallo", Text = "De tag", Time = DateTime.Now, Type = "Mark", Value = "Mark" }, ReviewEvent_PropertyChangedCallback));
private static void ReviewEvent_PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ReviewControl reviewControl = (ReviewControl)d;
reviewControl.LoadReviewEvent();
}
The strange thing is that when I run my progran the TagEvents get added to the collection and the ItemsControl shows the datatemplate for every item. The testblock I used to verify the value of the properties on the TagEvents shows the proper value of the text property. But in the ReviewEvent Dependency property I get only "empty" objects (all values empty string or default date). Those objects replace the default value as i can see that as the oldvalue in the DP callback.
I could understand the ItemsControl not showing the items, but why it is showing item that look like it's doing "new TagEvent" for every item in the collection is beyond me. Hope someone here has a suggestion for me. I tried implementing INotifyPropertyChanged on the TagEvent, but that did not seem to change anything. I could split out the properties of the TagEvent class but I don't see why I would have to do that, when I could pass the object.
Help?
I can now answer my own question. I had an statement setting the usercontrol's datacontext in code-behind that I forgot about. It messed things up.
Never leave old code laying about...
I am looking for a way to create an UserControl in silverlight 4 and expose a dependency property, which can accept any type. What I mean by that is, for example, if you look at standard silverlight control like AutoCompleteBox, it is capable of handling any type of collections. So you can bind AutoCompleteBox with IEnumerable<Human> or IENumerable<Animal> etc. And when any item is selected AutoCompleteBox returns the selected value either Human instance or Animal instance via SelectedItem dependency property.
I want to achieve similar flexibility with my usercontrol. I wouild like to expose 2 dependency properties SuggestedItems and SelectedItem. Which ever collection is set to SuggestedItems via consumers of this usercontrol thru Binding, lets take as an example IEnumerable<Car>, the I want SelectedItem property to send instance of Car type back to consumer thru Binding. If I used IEnumerable<Boat>, then I need Boat to be returned with SelectedItem.
I was trying to achieve it by using below example using MVVM, but its not working. I am looking for some clues as to how it should be designed, Am I even on a correct path or I have to completely alter my design?
I created an UserControl called VehicleSelectorUserControl which has its own dedicated ViewModel called VehicleSelectorViewModel with two proerties SuggestedItems, SelectedItem.
And usercontrol has corresponding Dependency properties in its codebehind to expose them to consumers of usercontrol. UserControl XAML has a ListBox which is bound to SuggestedItems property of VehicleSelectorViewModel. When user makes a selection, VehicleSelectorViewModel SelectedItem is set, which them invokes a delegate called ItemSelected to notify VehicleSelectorUserControl codebehind, which then sets the SelectedItem Dependency property to make it available to consumer.
Below is code from the VehicleSelectorUserControl.xaml.cs code behind.
private VehicleSelectorViewModel _TheViewModel;
public UserNameControl()
{
InitializeComponent();
_TheViewModel = Resources["TheViewModel"] as VehicleSelectorViewModel;
_TheViewModel.ItemSelected = OnItemSelected;
}
public IEnumerable<object> SuggestedItems
{
get { return (IEnumerable<object>)GetValue(SuggestedItemsProperty); }
set { SetValue(SuggestedItemsProperty, value); }
}
public static readonly DependencyProperty SuggestedItemsProperty =
DependencyProperty.Register("SuggestedItems", typeof(IEnumerable<object>), typeof(VehicleSelectorControl), new PropertyMetadata(OnSuggestedItemsSet));
private static void OnSuggestedItemsSet(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
_TheViewModel.SuggestedItems = e.NewValue;
}
public object SelectedItem
{
get { return (String) GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(object), typeof(VehicleSelectorControl), null);
private void OnItemSelected()
{
SelectedItem = _TheViewModel.SelectedItem;
}
Its ViewModel VehicleSelectorViewModel code
public Action ItemSelected { get; set; }
private dynamic _SelectedItem;
public dynamic SelectedItem
{
get { return _SelectedItem; }
set
{
if (value != _SelectedItem)
{
_SelectedItem = value;
NotifyPropertyChanged("SelectedItem");
if(ItemSelected != null) ItemSelected.Invoke();
}
}
}
private dynamic _SuggestedItems;
public dynamic SuggestedItems
{
get { return _SuggestedItems; }
set
{
if (value != _SuggestedItems)
{
_SuggestedItems = value;
NotifyPropertyChanged("SuggestedItems");
}
}
}
The XAML of consumer will look like (Consumer has its own ViewModel, which responsible for supplying SuggestedCars [IEnumerable<Car>], SuggestedBoats [IEnumerable<Boat>].
<my:VehicleSelectorControl x:Name="MyCarSelectorControl"
SuggestedItems="{Binding SuggestedCars, Mode=TwoWay}"
SelectedItem="{Binding UserSelectedCar, Mode=TwoWay}" />
<my:VehicleSelectorControl x:Name="MyBoatSelectorControl"
SuggestedItems="{Binding SuggestedBoats, Mode=TwoWay}"
SelectedItem="{Binding UserSelectedBoat, Mode=TwoWay}" />
I have a simple class that provides state codes like this:
public class StateProvider
{
static string[] _codes = new string[]
{
"AL",
"AK",
...
};
public string[] GetAll()
{
return _codes;
}
}
My model class that supports the view looks a little like this:
public class ProjectModel : ChangeNotifier
{
StateProvider _states = new StateProvider();
public ProjectModel()
{
Project = LoadProject();
}
ProjectEntity _project;
public ProjectEntity Project
{
get { return _project; }
set
{
_project = value;
FirePropertyChanged("Project");
}
}
public string[] States { get { return _states.GetAll(); } }
}
And my ComboBox XAML looks like this:
<ComboBox SelectedValue="{Binding Project.State, Mode=TwoWay}" SelectedValuePath="{Binding RelativeSource={RelativeSource Self}}" ItemsSource="{Binding States}" />
The binding works from the UI to the entity - if I select the state from the combo then the value gets pushed to the project entity and I can save it. However, if I shutdown and reload, the state code value doesn't bind from the entity to the UI and the combo shows nothing selected. Then, of course, a subsequent save nulls the entity's state value.
I want this very simple since I want to display state codes and save state codes (I don't want to display the full state name). So I don't want to have to muck with creating a State class that has Code and FullName properties and avoid having to use the SelectedValuePath and DisplayMemberPath properties of the combobox.
Edit:
Added to the code how ProjectModel does change notification. Note that the ProjectEntity class does this too. Trust me, it works. I've left it out because it also inherits from an Entity base class that does change notification through reflection. TwoWay binding works on everything but for the combobox.
You have to at least implement IPropertyNotifyChanged on your ProjectModel class
public class ProjectModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
and implement the Project property as below for binding to work other than 1-way-1-time.
public ProjectEntity Project
{
get { return (ProjectEntity)GetValue(ProjectProperty); }
set { SetValue(ProjectProperty, value); }
}
// Using a DependencyProperty as the backing store for Project.
// This enables animation, styling, binding, etc...
public static readonly DependencyProperty ProjectProperty =
DependencyProperty.Register("Project",
typeof(ProjectEntity),
typeof(ProjectModel),
new PropertyMetadata(null,
new PropertyChangedCallback(OnProjectChanged)));
static void OnProjectChanged(object sender, DependencyPropertyChangedEventArgs args)
{
// If you need to handle changes
}
Wow, whodathought it'd come down to this:
<ComboBox ItemsSource="{Binding States}" SelectedValue="{Binding Project.State, Mode=TwoWay}" />
It turned out it was the order in which I placed the attributes in the XAML. The SelectedValue binding was happening before the ItemsSource binding and thus there were no items in the combobox when the SelectedValue was bound.
Wow, this just seems like a really bad thing.
Change your ProjectModel class to this:
public class ProjectModel : ChangeNotifier
{
StateProvider _states = new StateProvider();
public ProjectModel()
{
Project = LoadProject();
States = new ObservableCollection<string>(_states.GetAll());
}
ProjectEntity _project;
public ProjectEntity Project
{
get { return _project; }
set
{
_project = value;
FirePropertyChanged("Project");
}
}
public ObservableCollection<string> States { get; set; }
}
Also make sure that ProjectEntity also implements INotifyPropertyChanged.