Can/Should I create instances of a UserControl in my viewmodel? - c#

Question regarding UserControls and MVVM. I have my wpf app with a main View/ViewModel. The viewmodel has a ObservableCollection of my usercontrol(s) that a listbox is bound to. The UserControl instances are added to the collection at run time based on events.
My question is if it's within the MVVM pattern to create the usercontrol objects from my main viewmodel? Example below in the onSomeEvent method is the code I'm unsure of, if this is where I should handle it? This doesn't feel right to me, but I'm still wrapping my mind around mvvm. Should I be adding the user control viewmodel here instead? Thanks for any guidance.
private ObservableCollection<string> myList = new ObservableCollection<string>();
public ObservableCollection<string> MyList
{
get { return myList; }
set
{
myList = value;
RaisePropertyChangedEvent("MyList");
}
}
public void onSomeEvent(string someData1, string someData2)
{
this.MyList.Add(new Views.MyUserControl(someData1, someData2));
}
Ok, I mocked up some code based on feedback from BradleyDotNET and dymanoid as I wrap my mind around it. Pasting it here to see if I'm on the right track.
I modified the listbox in my mainview xaml to add a template:
<ListBox Name="lbMain" Margin="10" ItemsSource="{Binding MyList, Mode=TwoWay}">
<ListBox.ItemTemplate>
<DataTemplate>
<WrapPanel>
<TextBlock Text="{Binding Test1}" FontWeight="Bold" />
<TextBlock Text="{Binding Test2}" FontWeight="Bold" />
</WrapPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
I then created a simple class like this to populate a few fields.
public class MyData
{
public MyData(string test1, string test2)
{
this.Test1 = test1;
this.Test2 = test2;
}
private string test1;
public string Test1
{
get
{
return test1;
}
set
{
test1 = value;
}
}
private string test2;
public string Test2
{
get
{
return test2;
}
set
{
test2 = value;
}
}
}
Then in my mainviewmodel I did this:
public void onSomeEvent(string someData1, string someData2)
{
this.MyList.Add(new MyData(someData1, someData2));
}

No, your viewmodel should not create any UserControl instances, because they are views. Furthermore, your main viewmodel shouldn't contain any collections of any views. As #BradleyDotNET mentioned, DataTemplate is the right way for it.
You should change your main viewmodel collection. It shouldn't contain any UserControls (views), but rather their viewmodels. Assuming that you have defined DataTemplates for your sub-viewmodels in XAML, you will get your views automagically created by WPF.
This could look like:
<DataTemplate DataType = "{x:Type local:UserControlViewModel}">
<local:UserControl/>
</DataTemplate>
With this approach, WPF sets the DataContext property value to the sub-viewmodel instance automatically, so you can easily define your bindings in that UserControl.

Related

Binding a Listbox to a ObservableCollection

So I am trying to bind the following ViewModel:
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<ListBoxItem> _PlacesOrCities;
public ObservableCollection<ListBoxItem> PlacesOrCities
{
get { return _PlacesOrCities; }
set { _PlacesOrCities = value; RaisePropertyChanged("PlacesOrCities"); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void RaisePropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public ViewModel()
{
_PlacesOrCities = new ObservableCollection<ListBoxItem>();
}
}
To the following xaml:
<ListBox Name="lbPlacesCity" ItemsSource="{Binding Path=(gms:MainWindow.ViewModel).PlacesOrCities, UpdateSourceTrigger=PropertyChanged}">
<ListBox.ItemTemplate>
<DataTemplate DataType="models:ListBoxItem">
<TextBlock Style="{StaticResource MaterialDesignBody2TextBlock}" Text="{Binding Name}" Visibility="{Binding Visibility}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
In the codebehind as such:
public ViewModel ViewModel { get; set; }
public MainWindow()
{
InitializeComponent();
ViewModel = new ViewModel();
DataContext = ViewModel;
}
And upon firing a button click event- I try to set the values of the observable collection using a in memory list:
private void StateProvince_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
_CurrentSelectionPlaces = Canada.Provinces
.FirstOrDefault(x => x.Abbreviation == _SelectedStateProvince_ShortName)
.Place.OrderBy(x => x.Name).ToList();
foreach (var currentSelectionPlace in _CurrentSelectionPlaces)
{
ViewModel.PlacesOrCities.Add(currentSelectionPlace);
}
}
But it seems like none of the items are being added to the collection. Am I binding it incorrectly?
I've tried quite a few solutions but none of them seem to change the result- where no items in the list are being loaded into the collection properly.
EDIT:
It may be worth noting that the ListBoxItem as seen in the ViewModel is a custom model:
public class ListBoxItem
{
[J("Name")] public string Name { get; set; }
[J("PostalCodes")] public string[] PostalCodes { get; set; }
public Visibility Visibility { get; set; } = Visibility.Visible;
}
You should try to fit to the MVVM pattern, so the population of the list should occur at viewmodel level and not in the view's code behind.
You mentioned that you use a click event, instead of doing so, try to bind the command property of the button to a command in the viewmodel, see this link with an explanation of several types of commands and how to use them: https://msdn.microsoft.com/en-us/magazine/dn237302.aspx
In the other hand, if you already set the data context in the window constructor, to bind the ListBox items source you only need the name of the property to bind, "PlacesOrCities":
<ListBox Name="lbPlacesCity" ItemsSource="{Binding Path=PlacesOrCities, UpdateSourceTrigger=PropertyChanged}">
<ListBox.ItemTemplate>
<DataTemplate DataType="models:ListBoxItem">
<TextBlock Style="{StaticResource MaterialDesignBody2TextBlock}" Text="{Binding Name}" Visibility="{Binding Visibility}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
It would also be recommendable trying to load the items in the list without any template, you can use ListBox DisplayMemberPath property to display the name, and once you are able to load items, apply the style.
Also in the way you use ObservableCollection, you actually need to replace the whole collection instead of adding to fire RaisePropertyChanged, try a normal property instead.
public ObservableCollection<ListBoxItem> PlacesOrCities {get;set;} = new ObservableCollection<ListBoxItem>();
Modifying the collection will update the UI, so whenever you use Add or Clear, the UI should know it.
Hope it helps.

MVVM Basic ContentControl Binding

I'm trying to figure out the correct way to bind a ViewModel to a ContentControl (I've looked all over the net but can't find an example that I can get to work correctly).
My Model:
public class Model
{
private string _Variable = "TEST";
public string Variable
{
get { return _Variable; }
set { _Variable = value; }
}
}
My ViewModel
public class ViewModel :ViewModelBase
{
private Model _Model = new Model();
public string Variable
{
get { return _Model.Variable; }
set
{
if (_Model.Variable != value)
{
_Model.Variable = value;
RaisePropertyChanged("Variable");
}
}
}
My View/Window
<Window.DataContext>
<local:ViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate DataType="{x:Type System:String}">
<TextBox/>
</DataTemplate>
</Window.Resources>
<StackPanel>
<ContentControl Content="{Binding Path=Variable}" />
</StackPanel>
So in essence, I have (or at least I believe I have) set the content of the ContentControl to the ViewModel property 'Variable', it is of type string so the only DataTemplate should be implemented and a Textbox displayed.
And that happens... A Textbox is displayed! However the Textbox is empty, and any changes made do not impact Variable.
This means I have made an error in the Batabinding, but I don't understand where. I have a feeling that just because my DataTemplate is displaying a Textbox, nothing is actually binding the string to it, but that is the bit I'm lost over.
Thanks for any help/advice.
You haven't specified the TextBox's Text binding, which is completely separate to the DataContext. Since you want the TextBox to bind to its own DataContext just do this:
<TextBox Text="{Binding Path=.}"/>
use textbox as below:
<TextBox Text="{Binding}" />

How to databind properties exposed from an ObservableCollection when the ObservableCollection is exposed from a view model?

I'm unable to find the correct binding syntax to data bind properties on items contained in an ObservableCollection when said ObservableCollection is exposed from a view model.
When I data bind the model's observable collection directly, it works. When I data bind the model's observable collection as exposed by the view model, it doesn't. This is a XAML/C# Windows Store 8.1 app.
What I currently get out of my skeleton test
{binding Path=ID}
{binding Path=Letter}
What I want
1
a
Here's my skeleton test
I have an Item class
Items are stored in the observable collection and I will ultimately want to bind against the ID and Letter properties:
public class Item
{
public Item(int id, char letter)
{
ID = id;
Letter = letter;
}
public int ID
{
get;
set;
}
public char Letter
{
get;
set;
}
}
The Model class contains an observable collection of items
public class Model
{
public Model()
{
}
public ObservableCollection<Item> Items
{
get
{
return _theItems;
}
set
{
_theItems = value;
}
}
ObservableCollection<Item> _theItems = new ObservableCollection<Item>();
}
I have a ViewModel that exposes the Model's observable collection
public class ViewModel
{
public ViewModel(Model model)
{
_model = model;
}
public ObservableCollection<Item> theItems
{
get
{
return _model.Items;
}
}
private Model _model;
}
My MainPage constructor creates the model with some test data, creates the ViewModel, and sets the page's DataContext to the view model:
public MainPage()
{
this.InitializeComponent();
Model model = new Model();
// some test data
model.Items.Add(new Item(1, 'a'));
_viewModel = new ViewModel(model);
this.DataContext = _viewModel;
}
My MainPage XAML looks like this
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<GridView ItemsSource="{Binding Path=theItems}" >
<GridView.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock>{binding Path=ID}</TextBlock>
<TextBlock>{binding Path=Letter}</TextBlock>
</StackPanel>
</DataTemplate>
</GridView.ItemTemplate>
</GridView>
</Grid>
This test skeleton is intended to simplify yet mimic my real goal which is to have a view model that exposes several things, one of which is an observable collection of objects whose different properties I want to bind. But binding the properties on items exposes from the observable collection eludes me.
I assume my problem relates to the XAML binding syntax. I am thinking that because I set the page's DataContext to the ViewModel, that setting the ItemsSource of the GridView to theItems would then allow me to refer to the ID and Letter properties of the items inside the collection.
When I directly bind the model's ObservableCollection to the MainPage's DataContext, all is well. But that departs from my goal with the ViewModel which is to expose additional things besides the ObservableCollection that I want to bind to from a singular view model.
I've tried various blog posts and things mentioned here on StackOverflow to no avail. Help appreciated.
Please check your XAML bindings.
...
<TextBlock Text="{Binding ID, Mode=OneTime}"/>
<TextBlock Text="{Binding Letter, Mode=OneTime}"/>
...
Consider to change the DataTemplate.
...
<DataTemplate>
<TextBlock>
<TextBlock.Inlines>
<Run Text="{Binding ID,Mode=OneTime}"/>
<Run Text=" - "/>
<Run Text="{Binding Letter,Mode=OneTime}"/>
</TextBlock.Inlines>
</TextBlock>
</DataTemplate>
...

ContentControl not updating

I'm trying to have a MainWindow that is bound to the a view. I change that view in code and expect it to update in the Main Window, however that is not happening.
I have this code in my XAML
<Grid>
<ContentControl Content="{Binding Source={StaticResource ViewModelLocator}, Path=MainWindowViewModel.CurrentControl}" />
</Grid>
I then change my Control via this code
public class MainWindowViewModel : ReactiveObject
{
private UserControl _CurrentControl = null;
public UserControl CurrentControl
{
get
{
if (_CurrentControl == null)
{
_CurrentControl = new HomePage();
}
return _CurrentControl;
}
set
{
this.RaiseAndSetIfChanged(x => x.CurrentControl, value);
}
}
}
As you can see I'm using the ReactiveUI library.
Is ContentControl the wrong thing to use in that view or am I just not binding and updating correctly?
There is actually a far better way to do this, using ViewModelViewHost:
<Grid DataContext="{Binding ViewModel, ElementName=TheUserControl}">
<ViewModelViewHost ViewModel="{Binding CurrentControlViewModel}" />
</Grid>
Now, your class will look something like:
public class MainWindowViewModel : ReactiveObject
{
private ReactiveObject _CurrentControlViewModel = new HomePageViewModel();
public ReactiveObject CurrentControlViewModel {
get { return _CurrentControl; }
set { this.RaiseAndSetIfChanged(x => x.CurrentControlViewModel, value); }
}
}
And somewhere in your app's startup, you should write:
RxApp.Register(typeof(IViewFor<HomePageViewModel>), typeof(HomePage));
What's ViewModelViewHost?
ViewModelViewHost will take a ViewModel object that you provide via Bindings, and look up a View that fits it, using Service Location. The Register call is how you can associate Views with ViewModels.
why you call your class MainWindowViewModel? when you wanna do mvvm you shouldn't have properties with type UserControl in your VM.
the usual mvvm way looks like this:
viewmodel with INotifyPropertyChanged
public class MyViewmodel
{
public IWorkspace MyContent {get;set;}
}
xaml content control with binding to your VM
<ContentControl Content="{Binding MyContent}"/>
datatemplate --> so that wpf knows how to render your IWorkspace
<DataTemplate DataType="{x:Type local:MyIWorkSpaceImplementationType}" >
<view:MyWorkspaceView />
</DataTemplate>
I think you have several muddled concepts here and they are getting in each others way.
Firstly you aren't actually using ANY of the reactiveUI code, it never gets called. Since your get accessor implements a lazy instantiation pattern then it means the set accessor is ignored. This means that the view never notifies the property change, so you never get updates.
I'd recommend using something more like
private UserControl _currentControl;
public MainWindowVirwModel()
{
CurrentControl = new HomePage();
}
public UserControl CurrentControl
{
get { return _curentControl;}
set { this.RaiseAndSetIfChanged(...); }
}
In addition, this still mixes up View components i.e. HomePage, inside your ViewModel tier which will making unit testing far more difficult.

WPF ComboBox Binding To Object On Initial Load

I have a combo box that is bound to a list of model objects. I've bound the combo box SelectedItem to a property that is the model type. All of my data binding works beautifully after the window has been loaded. The SelectedItem is set properly and I'm able to save the object directly with the repository.
The problem is when the window first loads I initialize the SelectedItem property and my combobox displays nothing. Before I moved to binding to objects I was binding to a list of strings and that worked just fine on initialization. I know I'm missing something but I can't figure it out.
Thanks in advance for any guidance you can provide.
(One note about the layout of this page. The combo boxes are actually part of another ItemTemplate that is used in a ListView. The ListView is bound to an observable collection in the main MV. Each item of this observable collection is itself a ModelView. It is that second ModelView that has the SelectedItem property.)
Here is my Model:
public class DistributionListModel : Notifier, IComparable
{
private string m_code;
private string m_description;
public string Code
{
get { return m_code; }
set { m_code = value; OnPropertyChanged("Code"); }
}
public string Name
{
get { return m_description; }
set { m_description = value; OnPropertyChanged("Name"); }
}
#region IComparable Members
public int CompareTo(object obj)
{
DistributionListModel compareObj = obj as DistributionListModel;
if (compareObj == null)
return 1;
return Code.CompareTo(compareObj.Code);
}
#endregion
}
Here the pertinent code in my ModelView:
public MailRoutingConfigurationViewModel(int agencyID)
: base()
{
m_agencyID = agencyID;
m_agencyName = DataManager.QueryEngine.GetAgencyName(agencyID);
IntializeValuesFromConfiguration(DataManager.MailQueryEngine.GetMailRoutingConfiguration(agencyID));
// reset modified flag
m_modified = false;
}
private void IntializeValuesFromConfiguration(RecordCheckMailRoutingConfiguration configuration)
{
SelectedDistributionList = ConfigurationRepository.Instance.GetDistributionListByCode(configuration.DistributionCode);
}
public DistributionListModel SelectedDistributionList
{
get { return m_selectedDistributionList; }
set
{
m_selectedDistributionList = value;
m_modified = true;
OnPropertyChanged("SelectedDistributionList");
}
}
And finally the pertinent XAML:
<UserControl.Resources>
<DataTemplate x:Key="DistributionListTemplate">
<Label Content="{Binding Path=Name}" />
</DataTemplate>
</UserControl.Resources>
<ComboBox
ItemsSource="{Binding Source={StaticResource DistributionCodeViewSource}, Mode=OneWay}"
ItemTemplate="{StaticResource DistributionListTemplate}"
SelectedItem="{Binding Path=SelectedDistributionList, Mode=TwoWay}"
IsSynchronizedWithCurrentItem="False"
/>
#SRM, if I understand correctly your problem is binding your comboBox to a collection of objects rather than a collection of values types ( like string or int- although string is not value type).
I would suggest add a two more properties on your combobox
<ComboBox
ItemsSource="{Binding Source={StaticResource DistributionCodeViewSource},
Mode=OneWay}"
ItemTemplate="{StaticResource DistributionListTemplate}"
SelectedItem="{Binding Path=SelectedDistributionList, Mode=TwoWay}"
SelectedValuePath="Code"
SelectedValue="{Binding SelectedDistributionList.Code }"/>
I am assuming here that DistributionListModel objects are identified by their Code.
The two properties I added SelectedValuePath and SelectedValue help the combobox identify what properties to use to mark select the ComboBoxItem by the popup control inside the combobox.
SelectedValuePath is used by the ItemSource and SelectedValue by for the TextBox.
don't call your IntializeValuesFromConfiguration from the constructor, but after the load of the view.
A way to achieve that is to create a command in your viewmodel that run this method, and then call the command in the loaded event.
With MVVM light toolkit, you can use the EventToCommand behavior... don't know mvvm framework you are using but there would probably be something like this.

Categories

Resources