Instantiating multiple view and passing data to each one - c#

I have a prism app where I am instantiating multiple views in the code-behind of a parent view based on a property in the viewmodel
public I2CNavigatorView()
{
InitializeComponent();
var viewModel = (I2CNavigatorViewModel) DataContext;
for (int i = 0; i < viewModel.NumberOfI2C; i++)
{
var i2CTabItem = new TabItem
{
Header = "I2C " + i,
Content = new I2CView(i)
};
NavigatorTabs.Items.Add(i2CTabItem);
}
}
and I need to pass the an index to the viewmodel of each child view, so my current solution is to pass the index to the view as a parameter when instantiating the view and setting a variable in the its viewmodel
public I2CView(int currentI2CIndex)
{
InitializeComponent();
var viewModel = (I2CViewModel) DataContext;
viewModel.CurrentI2CIndex = currentI2CIndex;
}
but I am not quite satisfied with the solution as the data flow path is: parent view -> child view ->
child viewmodel, while I need it to be: parent view -> child viewmodel
so I was thinking "Is there a way to pass the data directly to the view model when instantiating the view?"
Please advice,
Thanks in advance

I would start from the view model:
internal class I2CNavigatorViewModel
{
public IReadOnlyCollection<I2CViewModel> MyItems { get; }
}
<TabControl ItemsSource="{Binding MyItems}">
<TabControl.ContentTemplate>
<DataTemplate>
<I2CView />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
Then just initialize MyItems with the child view models, created with all the parameters you need.
Rant: a class doesn't want to have its name start with I, because it doesn't like to be mistaken for an interface!

Related

Binding ScrollView Content

I want to dynamically bind ScrollView content form ViewModel class.
I have xaml code:
<ScrollView
Grid.Column="1"
Content="{Binding UserGrid}"
HorizontalScrollBarVisibility="Never"
IsEnabled="False"
Orientation="Horizontal">
</ScrollView>
and ViewModel code
class ViewModel : BaseViewModel{
private Grid _userGrid { get; set; }
public Grid UserGrid
{
get { return _userGrid; }
set
{
_userGrid = value;
OnPropertyChanged();
}
}
public override async Task InitAsync()
{
await Task.Factory.StartNew(() =>
{
_userGrid = new Grid
{
ColumnSpacing = 1,
HorizontalOptions = LayoutOptions.FillAndExpand,
VerticalOptions = LayoutOptions.FillAndExpand
};
_userGrid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(GridHeightSize * 4) });
// and adding some content to This grid
}
}
Of course, I have initialized Binding context in View class
My question is if it is possible to bind this dynamically using this content binding.
Currently, I am getting an error:
,, No property, bindable property, or event found for 'Content', or mismatching type between value and property"
If it is not a proper way of dynamic view binding than what is?
The whole idea of databinding is to separate UI from data. Therefore, a data model should never contain any UI elements.
What you can do:
Create a new model class, which contains all the data you want to display inside that scroll view, for further reference i call it "CardViewModel" .
Add a property of List CardViewData to your ViewModel
Delete the InitAsync Task from your ViewModel
Make sure both of the view models implement the INotifyPropertyChanged interface
Now in the view consuming your ViewModel, overload the constructor to accept your view model as parameter.
Within the constructor you iterate through the items in the CardViewModel list and create a Grid for each item.
public TimeTableView(ViewModel model)
{
foreach (CardViewModel cardModel in CardViewData)
{
CardView view = new CardView(cardModel);
Container.Children.Add(view);
}
}
Note that "Container" is a placeholder for the element containing the cardviews.
Actually I would suggest that you implement a custom view, which contains all the Elements in order to display the data of your CardViewModel so you can set the binding context of the custom view to your cardview model:
public class CardView : Grid
{
public CardView(CardViewModel model)
{
BindingContext = model;
}
}
Also a ScrollView can only contain one element, so you can't simply throw a bunch of elements into it.
You probably need to put a grid inside it and then add the CardViews to that grid.

Prism: View First with multiple ViewModels

I'm using Prism with Unity IOC-Container in a WPF-Project. For all my other Views I'm using only one ViewModel per View. Because this View should be a Mask for both Input and Output of Data, I'd like to use two ViewModels.
For the current navigation to the View i use this Code:
_regionManager.RequestNavigate(RegionNames.ContentRegionName, typeof(Events).ToString());`
The Code Behind of my View:
public partial class Events : UserControl
{
public Events(EventsViewModel viewModel)
{
InitializeComponent();
}
}
One of the ViewModels:
public class EventsViewModel : BindableBase
{
public EventsViewModel()
{
// Some Code
}
// Some other Code
}
I heard about ViewModel Discovery, where you give the Constructor of the View an Interface instead of an actual ViewModel. But i could only find exacly this much information.
// Example of such a Method
public Events(IViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel
}
public Interface IViewModel
{
}
My question is now: How do I navigate to the View and tell it wich ViewModel it should get as DataContext? I'm relatively new to programming and the MVVM-Pattern and english is not my native language so maybe I missed some Information. I would be glad if someone had an answer for this. Thanks in advance.
Edit: Workaround
I came up with a workaround wich works for me. I used the method SetDefaultViewTypeToViewModelTypeResolver() from the ViewModelLocationProvider and customized it.
// Bootstrapper.cs
protected override void InitializeShell()
{
var window = (MainWindow)this.Shell;
Application.Current.MainWindow = window;
// Calling the method
ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver(ResolveViewModel);
var regionManager = Container.Resolve<IRegionManager>();
window._regionManager = regionManager;
globalRegionManager = regionManager;
regionManager.RegisterViewWithRegion(RegionNames.ContentRegionName, typeof(StartScreen));
regionManager.RegisterViewWithRegion(RegionNames.ContentRegionName, typeof(Stock));
window.Show();
}
// Property for handing over the desired ViewModel
public static Type DynamicViewModel { private get; set; }
private Type ResolveViewModel (Type viewType)
{
string _viewModel = null;
var name = viewType.FullName.Replace(".Views.", ".ViewModels.");
if (DynamicViewModel != null)
_viewModel = DynamicViewModel.ToString();
else
_viewModel = $"{name}ViewModel";
var fullName = IntrospectionExtensions.GetTypeInfo(viewType).Assembly.FullName;
var typeString = string.Format(CultureInfo.InvariantCulture, $"{_viewModel}, {fullName}");
DynamicViewModel = null;
return Type.GetType(typeString);
}
Then when I want to navigate, I hand over the ViewModel beforehand.
private void Button_Click(object sender, System.Windows.RoutedEventArgs e)
{
Bootstrapper.DynamicViewModel = typeof(EventsViewModel);
_regionManager.RequestNavigate(RegionNames.ContentRegionName, typeof(Events).ToString());
}
A little tricky but it seems to work without any Exceptions.
If there is a cleaner way I'm alway happy to here it. :)
Here are some techniques for getting the view model for a view (view first).
View discovery in views code behind constructor
public EventsView(EventsViewModel view_model)
{
InitializeComponent();
DataContext = view_model;
}
Explicitly newing up the view model in code behind constructor
public EventsView()
{
InitializeComponent();
DataContext = new EventsViewModel();
}
View model locator in the XAML for the view
<UserControl x:Class="EventsModule.Views.EventsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<TextBlock Text="{Binding EventName}"></TextBlock>
</Grid>
</UserControl>
As long as you register your view with the region manager using any one of the techniques below, any one of the above will work. Then navigate like you are doing in your question.
RegionManager.RegisterViewWithRegion(RegionNames.ContentRegionName, typeof(EventsView));
UnityContainer.RegisterType(typeof(object), typeof(EventsView), typeof(EventsView).FullName);
UnityContainer.RegisterTypeForNavigation<EventsView>(typeof(EventsView).FullName);
The first will activate the view in the region and is usually seen in the module Initialize method. Those last 2 are for registering a view for later navigation. The last one requires the Prism.Unity namespace.
I don't believe that a view can have more the one view model since the view object only has one DataContext property on it. You may have to extend one view model to include everything you need. Someone may come along and prove me wrong on this. I have seen where a view model is shared with more than one view, but never a view having more than one view model.

Change model instance at runtime?

Is it correct to change model instances in runtime? My control was bound to first instance, but during the program execution I d like to bind them to another instance.
somewheere in ViewModel class :
//ViewDefault - already initialized
// View - will be ready later
public string TextProperty
{
get
{
if (View != null)
{
return View.Model.text;
} return ViewDefault.Model.text;
}
set
{
if(View != null)
{
//.. logic with View.Model.text
}else{
// logic with ViewDefault.Model.text
}
RaiseOnPropertyChanged("TextProperty");
}
The question is - what I must do to notify my View that a binding content is changed?
<Setter Property="Text" Value="{Binding MyViewModel.TextProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
*ViewDefault.Model and View.Model have one type
In my point of view a ViewModel links a View with a Model so if I had to change the Model I would create a new ViewModel and attach it to the View's DataContext .
The view should be created by someone (a factory preferably) and the one creating the view should be creating the datacontext too and attaching it doing
View view = new View();
view.DataContext = new ViewModel();

Using MVVM instead of main window for the following code

I'm using the following code which is copy pasted from the main window which was working as expected ,
I have created View which is user control and put the code of the
code from the main window XAML
In the View model I put reference for the User model
In the user control I put the code for from the the main window which
is related to the event handlers for example the
DropText_PreviewDragEnter & listbox_SelectionChanged
Currently I have 2 issues in the User Control which Im not sure how to overcome...
1. Errors in the user control for all the occurrence of the ListBox (for example from listbox_SelectionChanged ystem.Windows.Controls.ListBox.SelectedItems.Count > 0 . the Selected items are marked at red with the following error
"cannot access non-static property SelectedItems item source in static context". ,not sure what is the reason since in the main window it was the same as static.
2. Since I have copied the code from the main window there is references to user object in the user controlwhich I believe is not acceptable in MVVM ,how should I change it ? for example
var mySelectedItem = System.Windows.Controls.ListBox.SelectedItem as User;
or
bool remove = _UsersList.Remove((User) System.Windows.Controls.ListBox.SelectedItem);
Here is the code.
I will appreciate your help !
The view model
public partial class ModelView : UserControl
{
private const string DRAG_SOURCE = "DragSource";
public ModelView()
{
InitializeComponent();
DataContext = new ModelView();
}
//Drag Over from text box to List box
private void ListBox_PreviewDrop(object sender, DragEventArgs e)
{
object dragSource = e.Data.GetData(DRAG_SOURCE);
if (dragSource != null && dragSource is TextBox)
{
(dragSource as TextBox).Text = String.Empty;
}
if (!String.IsNullOrEmpty(e.Data.GetData(DataFormats.StringFormat).ToString()) && dragSource is TextBox)
{
_UsersList.Add(new User {Name = e.Data.GetData(DataFormats.StringFormat).ToString()});
}
else
{
e.Handled = true;
}
}
}
}
The Xaml is
<TextBox x:Name="name1"
AcceptsReturn="True"
AllowDrop="True"
PreviewDragEnter="DropText_PreviewDragEnter"
PreviewDrop="DropText_PreviewDrop"
PreviewMouseDown="DropText_PreviewMouseDown"
HorizontalAlignment="Left" Height="20" Margin="360,70,0,0" TextWrapping="Wrap" Text=""
VerticalAlignment="Top" Width="70"/>
....
The model view
internal class ModelView
{
private ObservableCollection<User> _UsersList = new ObservableCollection<User>();
public ObservableCollection<User> UserList
{
get { return _UsersList; }
}
public void InitUsers()
{
_UsersList.Add(new User {Name = "fff"});
//Sort the User collection
ICollectionView usersView = CollectionViewSource.GetDefaultView(_UsersList);
usersView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
}
}
You already have two answers explaining why the first issue happend in the previous question. And follwoing points are what #Will said in comment as a mess in MVVM implementation that I can see in your codes :
UsersList in the model view is a Model as in Model-View-ViewModel.
And the model view it self is a ViewModel as in Model-View-ViewModel
Then what you call view model is actually a View in Model-View-ViewModel point of view. It inherits UserControl and UserControl is a view, no difference from Window or Page, etc. They're all View. And even if we agree to call it view model, then it violated MVVM principle everywhere, because view model shouldn't have reference to View/UI control object.
Not directly answering your question, but I hope you get a better prespective on MVVM pattern.
#phil correctly noted that you can't access the ListBox like this:
System.Windows.Controls.ListBox
What he failed to mention is that you shouldn't access a ListBox at all if you're using MVVM. Clearly you're not using MVVM now, but if you want to, then I would recommend that you read up on it so that you can get the full benefit from it. Just having a view and a view model does not mean that you're using MVVM.
In MVVM, we manipulate data, not UI controls. Therefore, you need to create a SelectedItem property in your view model and bind that to the ListBox.SelectedItem property and then you'll always have access to the item that is selected:
public User SelectedItem { get; set; } // Implement INotifyPropertyChanged here
...
<ListBox ItemsSource="{Binding YourCollection}" SelectedItem="{Binding SelectedItem}"/>
Now you can do something with the selected item like this:
string selectedItemName = SelectedItem.Name;
you have to access your listbox by
yourListBoxName.SelectedItems.Count > 0
you can't access it by
System.Windows.Controls.ListBox.SelectedItems.Count
same for
var mySelectedItem = System.Windows.Controls.ListBox.SelectedItem as User;
use the following instead
var mySelectedItem = yourListBoxName.SelectedItem as User;

How does a View know what ViewModel to use in WPF?

Can someone explain how the View and ViewModel are connected? I can't find anywhere the xaml or the xaml.cs for the View that references the ViewModel, nor anything in the ViewModel.cs file that references the View, yet they are somehow connected, and binding members from the ViewModel to the View work.
Also, in the constructor of each, there is only the InitializeComponent for the View and a basic constructor for the ViewModel (no declaration/definition of the View).
Thanks!
There are various options here.
Something has to set the View's DataContext to be an instance of the ViewModel. There are lots of options here:
This can be done directly in xaml (the View just instances the ViewModel directly).
This can be done in the View's constructor (this.DataContext = new MyViewModel();)
This can be handled via a DataTemplate
A "coordinating" class can wire these together (ie: a separate "presenter" class can construct both and set the DataContext appropriately)
The most common are to either have the View define the VM in the xaml (View-first), or to have everything based from a ViewModel-centric point of view, and have WPF automatically create the View based on the bound VM (ViewModel-first).
The former approach is what's used by a lot of toolkits, such as MVVM Light. The latter approach is what I used in my MVVM blog series, and used by some other toolkits.
A "clean" way for connecting the views to the view-models would be...
When you create the views, for each view, set its DataSource to its view-model:
E.g.
public class App
{
private void OnAppStart()
{
var model = new MainModel();
var vm = new MainVM();
var view = new MainWindow();
vm.Model = model;
view.DataSource = vm;
view.Show();
}
}
When the model you are viewing changes, update the VM:
public class MainVM
{
private void OnSelectedModelItemChanged()
{
this.SelectedItem = new ItemVM();
this.SelectedItem.Model = this.SelectedModelItem;
}
}
And use data templates to make view select the correct sub views for each VM.
The view contains an object of the view model class in the xaml.
The InitializeComponent function creates all the controls on the page, sets styles, etc.
As others have already shown, there are multiple options. Of course, whenever you hear of multiple options you have to wonder what are the advantages and disadvantages of each. Well, it just so turns out that all of them have major disadvantages except one.
The following approach involves no external libraries, no additional housekeeping classes and interfaces, almost no magic, and is very flexible because you can have viewmodels that contain other viewmodels, and you get to instantiate each one of them, so you can pass constructor parameters to them.
For the viewmodel of the main window:
using Wpf = System.Windows;
public partial class TestApp : Wpf.Application
{
protected override void OnStartup( Wpf.StartupEventArgs e )
{
base.OnStartup( e );
MainWindow = new MainView();
MainWindow.DataContext = new MainViewModel( e.Args );
MainWindow.Show();
}
}
For all other viewmodels:
This is in MainViewModel.cs:
using Collections = System.Collections.Generic;
public class MainViewModel
{
public SomeViewModel SomeViewModel { get; }
public OtherViewModel OtherViewModel { get; }
public Collections.IReadOnlyList<string> Arguments { get; }
public MainViewModel( Collections.IReadOnlyList<string> arguments )
{
Arguments = arguments;
SomeViewModel = new SomeViewModel( this );
OtherViewModel = new OtherViewModel( this );
}
}
This in MainView.xaml:
[...]
xmlns:local="clr-namespace:the-namespace-of-my-wpf-stuff"
[...]
<local:SomeView DataContext="{Binding SomeViewModel}" />
<local:OtherView DataContext="{Binding OtherViewModel}" />
[...]
As you can see, a viewmodel can simply be a member (child) of another viewmodel; in this case SomeViewModel and OtherViewModel are children of MainViewModel. Then, in the XAML file of MainView, you can just instantiate each of the child views and specify their DataContext by Binding to the corresponding child viewmodels.

Categories