I am trying to build a rather complex custom control.
I want to use recursion in my View Model to build out a control. I will try and be as clear as possible.
I have two classes, Publisher and Developer
The Publisher class looks like the following:
public class Publisher
{
public Publisher()
{
SubPublishers.CollectionChanged += SubPublishers_CollectionChanged;
ChildDevelopers.CollectionChanged += SubPublishers_CollectionChanged;
}
private ObservableCollection<Publisher> subPublishers;
public ObservableCollection<Publisher> SubPublishers
{
get
{
if (subPublishers == null)
subPublishers = new ObservableCollection<Publisher>();
return subPublishers;
}
}
private ObservableCollection<Developer> childDevelopers;
public ObservableCollection<Developer> ChildDevelopers
{
get
{
if (childDevelopers == null)
childDevelopers = new ObservableCollection<Developer>();
return childDevelopers;
}
}
And my Developer Class looks like this:
public class Developer : NotifyPropertyChanged
{
public Developer(string Title)
{
this.Title = Title;
}
private string title;
public string Title
{
get
{
return this.title;
}
set
{
this.title = value;
OnPropertyChanged("Title");
}
}
So yes, Publisher is n-tier. It can have a Collection of Developers and each of these Developers can have their own Collection of Developers.
Going to my Main View Model:
public class MainViewModel : NotifyPropertyChanged
{
public MainViewModel()
{
this.ParentPublisher = new ParentPublisher();
BuildData();
}
private Publisher parentPublisher;
public Publisher ParentPublisher
{
get
{
return this.parentPublisher;
}
set
{
this.parentPublisher = value;
OnPropertyChanged("ParentPublisher");
}
}
public void BuildData()
{
Publisher firstPublisher = new Publisher();
firstPublisher.ChildDevelopers.Add(new Developer("HAL"));
firstPublisher.ChildDevelopers.Add(new Developer("Retro Games"));
firstPublisher.ChildDevelopers.Add(new Developer("Nintendo"));
Publisher secondPublisher = new Publisher();
secondPublisher.ChildDevelopers.Add(new Developer("343"));
secondPublisher.ChildDevelopers.Add(new Developer("Playground Games"));
secondPublisher.SubPublishers.Add(new Publisher());
secondPublisher.SubPublishers.FirstOrDefault().ChildDevelopers.Add(new Developer("Coalition"));
secondPublisher.SubPublishers.FirstOrDefault().ChildDevelopers.Add(new Developer("Remedy"));
secondPublisher.SubPublishers.FirstOrDefault().SubPublishers.Add(new Publisher());
secondPublisher.SubPublishers.FirstOrDefault().SubPublishers.FirstOrDefault().ChildDevelopers.Add(new Developer("Insomniac"));
secondPublisher.SubPublishers.FirstOrDefault().SubPublishers.FirstOrDefault().ChildDevelopers.Add(new Developer("Criterion"));
secondPublisher.SubPublishers.FirstOrDefault().SubPublishers.FirstOrDefault().ChildDevelopers.Add(new Developer("EA"));
ParentPublisher.Add(firstPublisher);
ParentPublisher.Add(secondPublisher);
}
}
}
So, you can see the possible scenarios here. Now, I was trying to figure out how to build a control off this data.
I want to actually bind to the ParentPublisher because everything added (SubPublishers and the Child Developers) will ultimately be extension of the Parent.
Would I use an ObservableCollection and use the ItemSource to this ParentPublisher?
Any tips or recommendations would be appreciated.
One way is to make a UserControl that excepts the first tier as a DataTemplate. Then you show the data in the DataTemplate as needed. Inside the DataTemplate reference the UserControl again with the inner data.
Example: Obviously modify the layout to benefit you but for simplicity I did it this way.
<UserControl x:Class="WPF_Question_Answer_App.PublisherView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPF_Question_Answer_App"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<UserControl.Resources>
<DataTemplate x:Key="DeveloperTemplate">
<TextBlock Text="{Binding Title}" /> <!--show the developer data-->
</DataTemplate>
<DataTemplate x:Key="PublisherTemplate">
<local:PublisherView /> <!-- reference ourself for recursion-->
</DataTemplate>
</UserControl.Resources>
<StackPanel>
<ItemsControl ItemsSource="{Binding ChildDevelopers}"
ItemTemplate="{StaticResource DeveloperTemplate}" />
<ItemsControl ItemsSource="{Binding SubPublishers}"
ItemTemplate="{StaticResource PublisherTemplate}" />
</StackPanel>
</UserControl>
Related
I have a WPF MVVM project which has a property of type OudPattern.
I would like to bind this property to OudPatternEditor1 which is a UserControl in view. is there any way to do this using INotifyPropertyChange. Since I'm already using it in my project.
Please notice that OudPatternEditor1 is instantiated at the startup using a new OudPattern(). But this pattern can be changed by user. so I need to update this property in the view model and OudEditor1 also.
I solved the problem by passed the view to the view model and updating this property in there each time. But I know that this approach is against the MVVM principles of decoupling view and view model.
I also tried to solve this using dependency property but i failed to get a solution.
Custom user control XAML
<UserControl x:Class="MyOudTeacher.OudMachine.OudPatternEditor"
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" Visibility="Visible">
<Canvas x:Name="oudGridCanvas"/>
</UserControl>
Code-behind
public partial class OudPatternEditor : UserControl
{
private OudPattern oudPattern;
private double gridSquareWidth = 15;// my code: default 20
private double namesColumnWidth = 50;//my code: default 100
public OudPatternEditor()
{
InitializeComponent();
this.oudPattern = new OudPattern();
DrawNoteNames();
DrawPattern(namesColumnWidth);
DrawGridLines(namesColumnWidth);
}
public OudPattern OudPattern
{
get { return oudPattern; }
set {
oudPattern = value;
UpdateHitView();
}
}
/* more code... */
}
The view XAML
<UserControl x:Class="MyOudTeacher.OudMachine.OudMachineView"
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="500" xmlns:my="clr-namespace:MyOudTeacher.OudMachine">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="262*" />
</Grid.RowDefinitions>
<my:OudPatternEditor Grid.Row="1" HorizontalAlignment="Left" Margin="20" x:Name="oudPatternEditor1" VerticalAlignment="Top"/>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding StopCommand}" Margin="2" ToolTip="Stop">
<Rectangle Fill="DarkBlue" Width="15" Height="15" Margin="3" RadiusX="2" RadiusY="2"/>
</Button>
<!-- more code... -->
view model
class OudMachineViewModel : ViewModelBase, IDisposable
{
private IWavePlayer waveOut;
private OudPattern pattern;
private OudPatternEditor OudPatternEditor;
private OudPatternSampleProvider patternSequencer;
private int tempo;
private string selectedFile;
public ICommand PlayCommand { get; }
public ICommand StopCommand { get; }
public ICommand UpdateXmlCommand { get; }
public ICommand OpenFileCommand { get; }
public ICommand PauseCommand { get; }
public OudMachineViewModel(OudPatternEditor oudPatternEditor)
{
this.OudPattern = oudPatternEditor.OudPattern;
this.OudPatternEditor = oudPatternEditor;
Tempo = OudPattern.ScoreHits.ScaledTempo;
PlayCommand = new DelegateCommand(Play);
StopCommand = new DelegateCommand(Stop);
OpenFileCommand = new DelegateCommand(OpenFile);
PauseCommand = new DelegateCommand(Pause);
}
public OudPattern OudPattern
{
get { return pattern; }
set
{
pattern = value;
OnPropertyChanged("OudPattern");
}
}
private void UpdateSelectedFile()
{
OudPattern = new OudPattern(selectedFile);
OudPatternEditor.OudPattern = this.OudPattern;
Tempo = OudPattern.ScoreHits.ScaledTempo;
}
/* more code... */
The correct way is NOT to pass OudEditor to the view model constructor. since this is against MVVM. I expect to find a way to update OudEditor1.OudPattern by binding it to ViewModel.OudPattern
Edit #1:
I was able to add a dependency property and achieve what I wanted. The code added is simple:
view code-behind:
public OudPattern OudPattern
{
get { return (OudPattern)GetValue(OudPatternProperty); }
set
{
SetValue(OudPatternProperty, value);
DrawNewPattern();
UpdateHitView();
}
}
public static readonly DependencyProperty OudPatternProperty =
DependencyProperty.Register("OudPattern", typeof(OudPattern), typeof(OudPatternEditor),
new PropertyMetadata(default(OudPattern)));
view XAML above, modified:
<my:OudPatternEditor Grid.Row="1" HorizontalAlignment="Left" Margin="20" x:Name="oudPatternEditor1" VerticalAlignment="Top" OudPattern="{Binding OudPattern}"/>
view model:
public OudPattern OudPattern
{
get { return pattern; }
set
{
pattern = value;
OnPropertyChanged("OudPattern");
}
}
But I noticed that the setter for the dependency property in my view code-behind never executes. I learned that those setters are not executed when called from outside. How do I make the view code-behind react to external change and do the methods mentioned in the setter above? i.e.
DrawNewPattern();
UpdateHitView();
Edit# 2:
I found an answer to my last question about dependency property setter. Thanks for Elgonzo to point out that I can use property changed callbacks.
The code should be modified to add OnPatternChanged callback
public static readonly DependencyProperty OudPatternProperty =
DependencyProperty.Register("OudPattern", typeof(OudPattern), typeof(OudPatternEditor),
new PropertyMetadata(default(OudPattern),OnPatternChanged));
private static void OnPatternChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var myObject = (OudPatternEditor)d;
myObject.DrawNewPattern();
myObject.UpdateHitView();
Note that you cant call none static methods directly from static OnPatternChanged.. so you need to cast the sender into object and call its member methos.
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.
I am building a WPF application with mahapps, prism[modularity]. I have below HomeWindow.xaml code.
<Controls:MetroWindow x:Class="Project.Views.HomeWindow"
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:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro"
xmlns:local="clr-namespace:Project.Views"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
<!--The above code is for automatically binding of viewmodel into view-->
Height="700" Width="1200" Background="White">
<Grid>
<TabControl ItemsSource="{Binding TabCollection}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock>
<TextBlock Text="{Binding Name}"/>
</TextBlock>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<Label Content="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</Grid>
</Controls:MetroWindow>
I have below structure in my HomeViewModel.cs under ViewModels directory.
public class HomeViewModel : BindableBase
{
private ObservableCollection<Item> _tabCollection;
public ObservableCollection<Item> TabCollection { get { return _tabCollection; } set { SetProperty(ref _tabCollection, value); } }
//Prism way of getting and setting data
}
public class Item
{
private string Name;
private string Content;
public Item(string name, string content)
{
Name = name;
Content = content;
}
}
below is how I add data into TabCollection property through HomeWindow.xaml.cs.
private HomeViewModel _model=new HomeViewModel();
public HomeWindow(EmployeeViewModel model)
{
InitializeComponent();
_model.UserViewModel = model;
LoadHomeData(_model.UserViewModel.EmpRole);
DataContext = this;
}
private void LoadHomeData(string Role)
{
if (string.Equals(Role, "Admin"))
{
_model.TabCollection= new ObservableCollection<Item>()
{
new Item("Test1", "1"),
new Item("Test2", "2"),
new Item("Test3", "3")
};
}
}
Now matter what, the tabs will not get displayed. Its a blank empty window. I have followed the example in the issue here and have went through few similar posts having same kind of approach. But none of them helped. Is this because of prism way of databinding or is there anything else am missing here? Hope to find some help on this..
Your problem is not connected to MahApps or Prism but to how WPF works in general. In your case Name and Content are private fields and should be public properties
public string Name { get; set; }
public string Content { get; set; }
private or field is not a valid binding source. You can find more as to what is a valid binding source under Binding Sources Overview but in your case, as far as CLR object goes:
You can bind to public properties, sub-properties, as well as indexers, of any common language runtime (CLR) object. The binding engine uses CLR reflection to get the values of the properties. Alternatively, objects that implement ICustomTypeDescriptor or have a registered TypeDescriptionProvider also work with the binding engine.
Another problem is that DataContext is set wrong. At the moment is set to HomeWindow and I think it should be set to instance of HomeViewModel which holds TabCollection property
DataContext = _model;
I'm teaching myself WPF. My window has two combo boxes: one for Categories and one for Subcategories. When the category selection changes, I want the list of subcategories to update to just those that are in the selected category.
I've created a simple view class for both of the combo boxes. My SubcategoryView class' constructor takes a reference to my CategoryView class and attaches an event handler for when the category selection changes.
public class SubcategoryView : INotifyPropertyChanged
{
protected CategoryView CategoryView;
public SubcategoryView(CategoryView categoryView)
{
CategoryView = categoryView;
CategoryView.PropertyChanged += CategoryView_PropertyChanged;
}
private void CategoryView_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "SelectedItem")
{
_itemsSource = null;
}
}
private ObservableCollection<TextValuePair> _itemsSource;
public ObservableCollection<TextValuePair> ItemsSource
{
get
{
if (_itemsSource == null)
{
// Populate _itemsSource
}
return _itemsSource;
}
}
}
I assign my DataContexts like this.
cboCategory.DataContext = new CategoryView();
cboSubcategory.DataContext = new SubcategoryView(cboCategory.DataContext as CategoryView);
The problem is that selecting a new item in my category combo box does not cause the subcategories to repopulate (even though I confirmed my PropertyChanged handler is being called).
What is the correct way to cause the list to repopulate?
Also, I welcome any other comments about this approach. Instead of passing my CategoryView to the constructor, is it better to indicate this declaratively somehow in the XAML?
Here's how we do it in production code.
Each category knows what its subcategories are. If they're coming from a database or a disk file, the database/webservice method/file reader/whatever would return classes just like that, and you'd create the viewmodels to match. The viewmodel understands the structure of the information but knows and cares nothing about the actual content; somebody else is in charge of that.
Note that this is all very declarative: The only loop is the one that fakes up the demo objects. No event handlers, nothing in codebehind except creating the viewmodel and telling it to populate itself with fake data. In real life you do often end up writing event handlers for special cases (drag and drop, for example). There's nothing non-MVVMish about putting view-specific logic in the codebehind; that's what it's there for. But this case is much too trivial for that to be necessary. We have a number of .xaml.cs files that have sat in TFS for years on end exactly as the wizard created them.
The viewmodel properties are a lot of boilerplate. I have snippets (steal them here) to generate those, with the #regions and everything. Other people copy and paste.
Usually you'd put each viewmodel class in a separate file, but this is example code.
It's written for C#6. If you're on an earlier version we can change it to suit, let me know.
Finally, there are cases where it makes more sense to think in terms of having one combobox (or whatever) filtering another large collection of items, rather than navigating a tree. It can make very little sense to do that in this hierarchical format, particularly if the "category":"subcategory" relationship isn't one-to-many.
In that case, we'd have a collection of "categories" and a collection of all "subcategories", both as properties of the main viewmodel. We would then use the "category" selection to filter the "subcategory" collection, usually via a CollectionViewSource. But you could also give the viewmodel a private full list of all "subcategories" paired with a public ReadOnlyObservableCollection called something like FilteredSubCategories, which you'd bind to the second combobox. When the "category" selection changes, you repopulate FilteredSubCategories based on SelectedCategory.
The bottom line is to write viewmodels which reflect the semantics of your data, and then write views that let the user see what he needs to see and do what he needs to do. Viewmodels shouldn't be aware that views exist; they just expose information and commands. It's often handy to be able to write multiple views that display the same viewmodel in different ways or at different levels of detail, so think of the viewmodel as just neutrally exposing any information about itself that anybody might want to use. Usual factoring rules apply: Couple as loosely as possible (but no more loosely), etc.
ComboDemoViewModels.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace ComboDemo.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
{
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] String propName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
#endregion INotifyPropertyChanged
}
public class ComboDemoViewModel : ViewModelBase
{
// In practice this would probably have a public (or maybe protected) setter
// that raised PropertyChanged just like the other properties below.
public ObservableCollection<CategoryViewModel> Categories { get; }
= new ObservableCollection<CategoryViewModel>();
#region SelectedCategory Property
private CategoryViewModel _selectedCategory = default(CategoryViewModel);
public CategoryViewModel SelectedCategory
{
get { return _selectedCategory; }
set
{
if (value != _selectedCategory)
{
_selectedCategory = value;
OnPropertyChanged();
}
}
}
#endregion SelectedCategory Property
public void Populate()
{
#region Fake Data
foreach (var x in Enumerable.Range(0, 5))
{
var ctg = new ViewModels.CategoryViewModel($"Category {x}");
Categories.Add(ctg);
foreach (var y in Enumerable.Range(0, 5))
{
ctg.SubCategories.Add(new ViewModels.SubCategoryViewModel($"Sub-Category {x}/{y}"));
}
}
#endregion Fake Data
}
}
public class CategoryViewModel : ViewModelBase
{
public CategoryViewModel(String name)
{
Name = name;
}
public ObservableCollection<SubCategoryViewModel> SubCategories { get; }
= new ObservableCollection<SubCategoryViewModel>();
#region Name Property
private String _name = default(String);
public String Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
OnPropertyChanged();
}
}
}
#endregion Name Property
// You could put this on the main viewmodel instead if you wanted to, but this way,
// when the user returns to a category, his last selection is still there.
#region SelectedSubCategory Property
private SubCategoryViewModel _selectedSubCategory = default(SubCategoryViewModel);
public SubCategoryViewModel SelectedSubCategory
{
get { return _selectedSubCategory; }
set
{
if (value != _selectedSubCategory)
{
_selectedSubCategory = value;
OnPropertyChanged();
}
}
}
#endregion SelectedSubCategory Property
}
public class SubCategoryViewModel : ViewModelBase
{
public SubCategoryViewModel(String name)
{
Name = name;
}
#region Name Property
private String _name = default(String);
public String Name
{
get { return _name; }
set
{
if (value != _name)
{
_name = value;
OnPropertyChanged();
}
}
}
#endregion Name Property
}
}
MainWindow.xaml
<Window
x:Class="ComboDemo.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ComboDemo"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<StackPanel Orientation="Vertical" Margin="4">
<StackPanel Orientation="Horizontal">
<Label>Categories</Label>
<ComboBox
x:Name="CategorySelector"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="20,4,4,4">
<Label>Sub-Categories</Label>
<ComboBox
ItemsSource="{Binding SelectedCategory.SubCategories}"
SelectedItem="{Binding SelectedCategory.SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs
using System.Windows;
namespace ComboDemo
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var vm = new ViewModels.ComboDemoViewModel();
vm.Populate();
DataContext = vm;
}
}
}
Extra Credit
Here's a different version of MainWindow.xaml, which demonstrates how you can show the same viewmodel in two different ways. Notice that when you select a category in one list, that updates SelectedCategory which is then reflected in the other list, and the same is true of SelectedCategory.SelectedSubCategory.
<Window
x:Class="ComboDemo.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ComboDemo"
xmlns:vm="clr-namespace:ComboDemo.ViewModels"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525"
>
<Window.Resources>
<DataTemplate x:Key="DataTemplateExample" DataType="{x:Type vm:ComboDemoViewModel}">
<ListBox
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type vm:CategoryViewModel}">
<StackPanel Orientation="Horizontal" Margin="2">
<Label Width="120" Content="{Binding Name}" />
<ComboBox
ItemsSource="{Binding SubCategories}"
SelectedItem="{Binding SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="120"
/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DataTemplate>
</Window.Resources>
<Grid>
<StackPanel Orientation="Vertical" Margin="4">
<StackPanel Orientation="Horizontal">
<Label>Categories</Label>
<ComboBox
x:Name="CategorySelector"
ItemsSource="{Binding Categories}"
SelectedItem="{Binding SelectedCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="20,4,4,4">
<Label>
<TextBlock Text="{Binding SelectedCategory.Name, StringFormat='Sub-Categories in {0}:', FallbackValue='Sub-Categories:'}"/>
</Label>
<ComboBox
ItemsSource="{Binding SelectedCategory.SubCategories}"
SelectedItem="{Binding SelectedCategory.SelectedSubCategory}"
DisplayMemberPath="Name"
MinWidth="200"
/>
</StackPanel>
<GroupBox Header="Another View of the Same Thing" Margin="4">
<!--
Plain {Binding} just passes along the DataContext, so the
Content of this ContentControl will be the main viewmodel.
-->
<ContentControl
ContentTemplate="{StaticResource DataTemplateExample}"
Content="{Binding}"
/>
</GroupBox>
</StackPanel>
</Grid>
</Window>
Using single view-model in that case is really simpler, as mentioned in comments. For example, I'll use just strings for combo box items.
To demonstrate correct using of view model, we'll track changes of category through binding rather than UI event. So, besides ObservableCollections you'll need SelectedCategory property.
View-model:
public class CommonViewModel : BindableBase
{
private string selectedCategory;
public string SelectedCategory
{
get { return this.selectedCategory; }
set
{
if (this.SetProperty(ref this.selectedCategory, value))
{
if (value.Equals("Category1"))
{
this.SubCategories.Clear();
this.SubCategories.Add("Category1 Sub1");
this.SubCategories.Add("Category1 Sub2");
}
if (value.Equals("Category2"))
{
this.SubCategories.Clear();
this.SubCategories.Add("Category2 Sub1");
this.SubCategories.Add("Category2 Sub2");
}
}
}
}
public ObservableCollection<string> Categories { get; set; } = new ObservableCollection<string> { "Category1", "Category2" };
public ObservableCollection<string> SubCategories { get; set; } = new ObservableCollection<string>();
}
Where SetProperty is implementation of INotifyPropertyChanged.
When you select category, the setter of SelectedCategory property triggers and you can fill subcatagory items depending on selected category value. Do not replace collection object itself! You should clear existing items and then add new ones.
In xaml, besides ItemsSource for both combo boxes, you'll need bind SelectedItem for category combo box.
XAML:
<StackPanel x:Name="Wrapper">
<ComboBox ItemsSource="{Binding Categories}" SelectedItem="{Binding SelectedCategory, Mode=OneWayToSource}" />
<ComboBox ItemsSource="{Binding SubCategories}" />
</StackPanel>
Then just assign view-model to wrapper's data context:
Wrapper.DataContext = new CommonViewModel();
And code for BindableBase:
using System.ComponentModel;
using System.Runtime.CompilerServices;
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Equals(storage, value))
{
return false;
}
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
In my solution; I have two projects: One is a WPF UserControl Library, and the other is a WPF Application.
The usercontrol is pretty straightforward; it's a label and a combo box that will show the installed printers.
In the WPF application; I want to use this usercontrol. The selected value will be stored in user settings.
The problem I'm having is that I can't seem to get the proper binding to work. What I need to happen is to be able to set the SelectedValue of the UserControl when the MainWindow loads; as well as access the SelectedValue of the UserControl when I go to save my settings.
My code is below, could someone point me in the right direction?
PrintQueue user control:
<UserControl x:Class="WpfControls.PrintQueue"
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" xmlns:wpfControls="clr-namespace:WpfControls"
mc:Ignorable="d">
<UserControl.DataContext>
<wpfControls:PrintQueueViewModel/>
</UserControl.DataContext>
<Grid>
<StackPanel Orientation="Horizontal">
<Label Content="Selected Printer:"></Label>
<ComboBox ItemsSource="{Binding Path=PrintQueues, Mode=OneWay}" DisplayMemberPath="Name" SelectedValuePath="Name" Width="200" SelectedValue="{Binding Path=SelectedPrinterName, Mode=TwoWay}"></ComboBox>
</StackPanel>
</Grid>
</UserControl>
Print Queue Codebehind:
public partial class PrintQueue : UserControl
{
public static readonly DependencyProperty CurrentPrinterNameProperty =
DependencyProperty.Register("CurrentPrinterName", typeof (string), typeof (PrintQueue), new PropertyMetadata(default(string)));
public string CurrentPrinterName
{
get { return (DataContext as PrintQueueViewModel).SelectedPrinterName; }
set { (DataContext as PrintQueueViewModel).SelectedPrinterName = value; }
}
public PrintQueue()
{
InitializeComponent();
DataContext = new PrintQueueViewModel();
}
}
PrintQueue View Model:
public class PrintQueueViewModel : ViewModelBase
{
private ObservableCollection<System.Printing.PrintQueue> printQueues;
public ObservableCollection<System.Printing.PrintQueue> PrintQueues
{
get { return printQueues; }
set
{
printQueues = value;
NotifyPropertyChanged(() => PrintQueues);
}
}
private string selectedPrinterName;
public string SelectedPrinterName
{
get { return selectedPrinterName; }
set
{
selectedPrinterName = value;
NotifyPropertyChanged(() => SelectedPrinterName);
}
}
public PrintQueueViewModel()
{
PrintQueues = GetPrintQueues();
}
private static ObservableCollection<System.Printing.PrintQueue> GetPrintQueues()
{
var ps = new PrintServer();
return new ObservableCollection<System.Printing.PrintQueue>(ps.GetPrintQueues(new[]
{
EnumeratedPrintQueueTypes.Local,
EnumeratedPrintQueueTypes.Connections
}));
}
}
Main Window:
<Window x:Class="WPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfControls="clr-namespace:WpfControls;assembly=WpfControls" xmlns:wpfApp="clr-namespace:WPFApp"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<wpfApp:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<wpfControls:PrintQueue CurrentPrinterName="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=DataContext.PrinterName, Mode=TwoWay}"></wpfControls:PrintQueue>
</StackPanel>
</Grid>
</Window>
Main Window View Model:
public class MainWindowViewModel : ViewModelBase
{
private string printerName;
public string PrinterName
{
get { return printerName; }
set
{
printerName = value;
NotifyPropertyChanged(() => PrinterName);
}
}
public MainWindowViewModel()
{
PrinterName = "Lexmark T656 PS3";
}
}
Controls in a library need to expose DependencyProperties that you can bind to in your view. Just like WPF's TextBox exposes a Text property.
Your PrintQueue control doesn't expose anything, and instead keeps all its state in a viewmodel that nothing outside can access. Your MainWindowViewModel has no way of getting at the stuff inside PrintQueueViewModel.
You need to expose SelectedPrinterName as a DependencyProperty in the code behind of your PrintQueue xaml. Then in MainWindow.xaml you can bind it to MainWindowViewModel.PrinterName.
If you want to user ViewModels all the way through instead, then MainWindowViewModel should be creating PrintQueueViewModel itself so it can access the properties within.
As per your update / comment:
Unfortunately DependencyProperties don't work like that. The getters/setters aren't even used most of the time, and they should ONLY update the property itself. You're sort of halfway between two worlds at the moment.
If I were in your position, and assuming you can change the library so PrintQueue.xaml doesn't have a hardcoded VM instance in the view, I would just create the PrintQueueViewModel yourself. That's how MVVM is supposed to work:
ViewModel:
public class MainWindowViewModel : ViewModelBase
{
public PrintQueueViewModel PrintQueue { get; private set; }
public MainWindowViewModel()
{
PrintQueue = new PrintQueueViewModel();
PrintQueue.SelectedPrinterName = "Lexmark T656 PS3";
}
}
View:
<Window x:Class="WPFApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wpfControls="clr-namespace:WpfControls;assembly=WpfControls" xmlns:wpfApp="clr-namespace:WPFApp"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<wpfApp:MainWindowViewModel/>
</Window.DataContext>
<Grid>
<StackPanel>
<wpfControls:PrintQueue DataContext="{Binding PrintQueue}"/>
</StackPanel>
</Grid>
</Window>
Again though, control libraries generally don't have view models, and expose their state via dependency properties since they're designed to be used in XAML.
Component libraries may expose view models, but in that case they wouldn't hard code the view model in the view.
Did you write the library? If not, how did the author expect people to use it?
I think with this small changes everything should work
<ComboBox ItemsSource="{Binding Path=PrintQueues, Mode=OneWay}" DisplayMemberPath="Name" Width="200" SelectedItem="{Binding Path=SelectedPrinter, Mode=TwoWay}"></ComboBox>
private System.Printing.PrintQueue selectedPrinter;
public System.Printing.PrintQueue SelectedPrinter
{
get { return selectedPrinter; }
set
{
selectedPrinter = value;
NotifyPropertyChanged(() => SelectedPrinter);
}
}
Now from the main window you can modify SelectedPrinter on the viewmodel and the change should be reflected on the view
(PrintQueue.DataContext as PrintQueueViewModel).SelectedPrinter = ...
I tried your code and your bindings of the PrintQueueView to the corresponding view model work fine. Your problem is that the MainWindowViewModel does not know about the PrintQueueViewModel and thus cannot retrieve the value of the selected printer when the main window closes (I guess that is the scenario you want to implement).
The quickest solution to your problem would be to do the following steps:
In MainWindow.xaml, give PrintQueue a Name so you can access it in the code behind
In MainWindow.xaml.cs, override the OnClosing method. In it you can retrieve the view model as follows: var viewModel = (PrintQueueViewModel)PrintQueue.DataContext;. After that you can retrieve the selected value and save it or whatever.
In the MainWindow constructor after InitializeComponent, you can retrieve your saved value from a file and set it on the PrintQueueViewModel by retrieving it the same way as in the previous step.
Whole code in MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// Retrieve your selected printer here; in this case, I just set it directly
var selectedPrinter = "Lexmark T656 PS3";
var viewModel = (PrintQueueViewModel)PrintQueue.DataContext;
viewModel.SelectedPrinterName = selectedPrinter;
}
protected override void OnClosing(CancelEventArgs e)
{
var viewModel = (PrintQueueViewModel)PrintQueue.DataContext;
var selectedPrinterName = viewModel.SelectedPrinterName;
// Save the name of the selected printer here
base.OnClosing(e);
}
}
Please remember that the major point of view models is the ability to unit-test GUI logic and to disconnect GUI appearance and logic. Your view models should not be able to retrieve all the possible printers of your system but should obtain these values by e.g. Dependency Injection. I would advise you to read about SOLID programming.