How to manage content for multiple TabControlItems in a Prism application? - c#

I am witting a new WPF application using C# with the help of Prism 6. and the MVVM design pattern.
I have a main windows with a Top, Right, and a Center region. On the top region I have a toolbar, when a user clicks "Show Message" button, I show a view called "Message" in the center region. I am able to do that using RegionManager.RequestNavigate method to show the "Message" view which is working fine.
However, my "Message" view have multiple tabs. When the user clicks on the TabControlItems, I want to be able to show different views in the tab-Content.
Here is how my Message view look like. The idea here is to have a collection of ViewModels, and display the tabs according to the collection.
<TabControl ItemsSource="{Binding ViewModelCollection}"
Background="Transparent">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<prism:InvokeCommandAction Command="{Binding TabSelectionChangedCommand}"
CommandParameter="{Binding ViewName}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabTitle}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl prism:RegionManager.RegionName="{x:Static foundation:RegionNames.TabContentRegionName}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
Then in my MessageViewModel I have the following code
public class MessageViewModel : BindableBase
{
public ObservableCollection<TabBasedNavigationAwareViewModel> ViewModelCollection { get; set; }
public DelegateCommand<string> ChangeTabContent { get; set; }
public MessageViewModel(IUnitOfWork unitOfWork, IPassportManager passportManager, ICoreRegionManager regionManager, IUnityContainer container)
: base(unitOfWork, passportManager, regionManager)
{
ViewModelCollection = new ObservableCollection<TabBasedNavigationAwareViewModel>();
ViewModelCollection.Add(container.Resolve<FirstViewModel>());
ViewModelCollection.Add(container.Resolve<SecondViewModel>());
ChangeTabContent = new DelegateCommand<string>(HandleChangeContent, CanChangeContent);
}
protected bool CanChangeContent(string viewName)
{
return true;
}
protected void HandleChangeContent(string viewName)
{
IRegion region = RegionManager.Regions[RegionNames.TabContentRegionName];
region.RequestNavigate(new Uri("Modules.Messages.Views." + viewName, UriKind.Relative));
}
public override void OnNavigatedTo(NavigationContext navigationContext)
{
ChangeTabContent.Execute("FirstView");
}
}
The problem is when the application starts I get an error
The region manager does not contain the TabContent region.
I clearly understand the error and why it is happening. But not sure how to solve it. May be Regions is the wrong way to do when using tab-controls inside a main region. I also tried to call the following code from the MessageViewModel constructor RegionManager.RegisterViewWithRegion(RegionNames.TabContentRegionName, typeof(MessageView));
What is the correct way to manage/display the correct view when the user click on the tabs?
Please note that I am using Fody.PropertyChanged package so it automatically notify when the property changed.
UPDATED
I tried to remove regions from view, now I get the tabs and the content screen is showing the FirstViewModel full name instead of the corresponding view.
Here is my code without regions
public class MessageViewModel : BindableBase
{
public ObservableCollection<TabBasedNavigationAwareViewModel> ViewModelCollection { get; set; }
public MessageViewModel(IUnitOfWork unitOfWork, IPassportManager passportManager, ICoreRegionManager regionManager, IUnityContainer container)
: base(unitOfWork, passportManager, regionManager)
{
ViewModelCollection = new ObservableCollection<TabBasedNavigationAwareViewModel>();
ViewModelCollection.Add(container.Resolve<FirstViewModel>());
ViewModelCollection.Add(container.Resolve<SecondViewModel>());
}
}
Here is the view
<TabControl ItemsSource="{Binding ViewModelCollection}"
Background="Transparent">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabTitle}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>

Also add some DataTemplates for your tab view models to a resource dictionary nearby
<DataTemplate DataType="{x:Type FirstViewModel}">
<TextBlock Text="{Binding SomeContent}"/>
</DataTemplate>

Related

Prism Parent TabControl Child Regions

Trying to create a TabControl Region inside another Region. The TabControl has a set number of Views that will be added to it, with their own respective ViewModels.
But either the View doesn't show up, the tabitem doesn't show up with only one View displayed instead, or I get the following error:
System.ArgumentException: 'This RegionManager does not contain a Region with the name 'ParentTabRegion'. (Parameter 'regionName')'
MainMenuView:
<Grid>
<ContentControl prism:RegionManager.RegionName="ContentRegion" />
</Grid>
MainMenuViewModel:
public class MainMenuViewModel : BindableBase
{
private readonly IRegionManager _regionManger;
public MainMenuViewModel(IRegionManager regionManager)
{
_regionManger = regionManager;
_regionManger.RequestNavigate("ContentRegion", "ParentView");
}
}
ParentView:
<Grid>
<TabControl prism:RegionManager.RegionName="ParentTabRegion" />
</Grid>
ParentViewModel:
public class ParentViewModel : BindableBase
{
private readonly IRegionManager _regionManger;
private Child1View _tab1 = new Child1View();
private Child1View Tab1
{
get { return _tab1; }
set { SetProperty(ref _tab1, value); }
}
private Child2View _tab2 = new Child2View();
private Child2View Tab2
{
get { return _tab2; }
set { SetProperty(ref _tab2, value); }
}
public ParentViewModel(IRegionManager regionManger)
{
_regionManger = regionManger;
// Gives 'This RegionManager does not contain a Region with the name 'GeneralDataTabRegion'. (Parameter 'regionName')' error
_regionManger.AddToRegion("ParentTabRegion", typeof(Child1View));
_regionManger.AddToRegion("ParentTabRegion", typeof(Child2View));
//I've also tried the following
// Same error as above
// _regionManger.Regions["ParentTabRegion"].Add(typeof(Tab1View));
// _regionManger.Regions["ParentTabRegion"].Add(typeof(Tab2View));
// Same error as above
// _regionManger.AddToRegion("ParentTabRegion", Tab1);
// _regionManger.AddToRegion("ParentTabRegion", Tab2);
// Only the last registered view is displayed
// _regionManger.RegisterViewWithRegion("ParentTabRegion", typeof(Tab1));
// _regionManger.RegisterViewWithRegion("ParentTabRegion", typeof(Tab2));
}
}
I also have the prism namespace in all the views:
xmlns:prism="http://prismlibrary.com/"
prism:ViewModelLocator.AutoWireViewModel="True"
Maybe I'm not registering the ParentTabRegion somehow? But I don't have to register the other regions and they seem to just work out of the box.
Let me know if you know what I'm doing wrong or if there is something I'm missing. Thank you.
I would just comment but can't due to low reputation. Anyway..
Check this post
Prism 7 throws and exception when working with nested views
As stated in the comments: "the problem is about how to inject scope region in ViewModel"
This video from Brian should help you with the issue.
https://app.pluralsight.com/library/courses/prism-mastering-tabcontrol
I tested some other things out. Since I don't need dynamic tabs, I found this to be the cleanest solution using Prism:
Parent ViewModel:
public ParentViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
_regionManager.RegisterViewWithRegion("ChildRegion", typeof(Child1View));
_regionManager.RegisterViewWithRegion("ChildRegion", typeof(Child2View));
}
Parent View:
<UserControl.Resources>
<Style TargetType="TabItem">
<Setter Property="Header"
Value="{Binding DataContext.Title}"/>
</Style>
</UserControl.Resources>
<Grid>
<TabControl prism:RegionManager.RegionName="ChildRegion" />
</Grid>
I ended up doing this a bit differently since I don't really need to dynamically add Tabs.
So what I ended up doing was just adding all the ViewModels to an ObservableCollection of BindableBase. Then I just added them to the view using a DataTemplate.
Parent ViewModel:
private ObservableCollection <BindableBase> _childTabs;
public ObservableCollection <BindableBase> ChildTabs
{
get { return _childTabs; }
set { _childTabs = value; }
}
public ParentViewModel()
{
ChildTabs = new ObservableCollection <BindableBase> {
new Child1ViewModel(),
new Child2ViewModel()
};
}
Parent View:
<TabControl ItemsSource="{Binding ChildTabs}"
SelectedIndex="0">
<TabControl.Resources>
<DataTemplate DataType="{x:Type vm:Child1ViewModel}">
<view:Child1 />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:Child2ViewModel}">
<view:Child2 />
</DataTemplate>
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
I still feel like I'm doing something wrong though, this doesn't feel like MVVM to me...

WPF Main menu for different views / viewmodels

I'd like to create an app, containing the main menu (ribbonmenu) and different usercontrols, each assigned to an own ViewModel.
I was told to not implement classic events in code-behind but to use commands. So far, everything fine, commands for needed methods are implemented.
In my previous approach I "loaded" the UserControl, by assigning the corresponding ViewModel to a ContentControl, that loaded the UserControl, that was assigned to the ViewModel in MainWindow.Resource.
My last approach, simplified with a button instead of a menu:
<Window.Resources>
<DataTemplate x:Name="settingsViewTemplate" DataType="{x:Type viewmodels:SettingsViewModel}">
<views:SettingsView DataContext="{Binding SettingsVM, Source={StaticResource Locator}}"/>
</DataTemplate>
<DataTemplate x:Name="projectsViewTemplate" DataType="{x:Type viewmodels:ProjectViewModel}">
<views:ProjectView DataContext="{Binding ProjectVM, Source={StaticResource Locator}}"/>
</DataTemplate>
</Window.Resources>
<StackPanel>
<Button Content="Load Settings" Height="20" Margin="20 20 20 0" Click="ShowSettings"/>
<ContentControl Margin="5" Height="100" Content="{Binding}"/>
</StackPanel>
simplified code-behind:
public SettingsViewModel settingsViewModel;
public MainWindow()
{
InitializeComponent();
settingsViewModel = new SettingsViewModel();
}
private void ShowSettings(object sender, RoutedEventArgs e)
{
DataContext = settingsViewModel;
}
How can I load a UserControl, using ViewModel commands?
Don't use code-behind to handle view models. A View model should handle view models. Generally the same view model that implements the commands.
First create a main view model for the MainWindow as data source. This view model will also handle the switching between the views. It's recommended to let all page view models implement a common base type e.g. IPage.
Also you don't need any locator for this scenario. The views inside the DataTemplate will automatically have their DataContext set to the data type that maps to the DataTemplate. SettingsView will automatically have SetingsViewModel as the DataContext. If this would be the wrong context, then your model design is wrong.
IPage.cs
interface IPage : INotifyPropertyChanged
{
string PageTitel { get; set; }
}
SettingsViewModel.cs
class SettingsViewModel : IPage
{
...
}
ProjectViewModel.cs
class ProjectViewModel : IPage
{
...
}
PageName.cs
public enum PageName
{
Undefined = 0, SettingsPage, ProjectPage
}
MainViewModel.cs
An implementation of RelayCommand can be found at
Microsoft Docs: Patterns - WPF Apps With The Model-View-ViewModel Design Pattern - Relaying Command Logic
class MainViewModel : INotifyPropertyChanged
{
public ICommand SelectPageCommand => new RelayCommand(SelectPage);
public Dictionary<PageName, IPage> Pages { get; }
private IPage selectedPage;
public IPage SelectedPage
{
get => this.selectedPage;
set
{
this.selectedPage = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
this.Pages = new Dictionary<PageName, IPage>
{
{ PageName.SettingsPage, new SettingsViewModel() },
{ PageName.ProjectPage, new ProjectViewModel() }
};
this.SelectedPage = this.Pages.First().Value;
}
public void SelectPage(object param)
{
if (param is PageName pageName
&& this.Pages.TryGetValue(pageName, out IPage selectedPage))
{
this.SelectedPage = selectedPage;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MainWindow.xaml
<Window>
<Window.DataContext>
<MainViewModel />
</Window.DataContext>
<Window.Resources>
<DataTemplate x:Name="settingsViewTemplate" DataType="{x:Type viewmodels:SettingsViewModel}">
<views:SettingsView />
</DataTemplate>
<DataTemplate x:Name="projectsViewTemplate" DataType="{x:Type viewmodels:ProjectViewModel}">
<views:ProjectView />
</DataTemplate>
</Window.Resources>
<StackPanel>
<!-- Content navigation -->
<StackPanel Orientation="Horizontal">
<Button Content="Load Settings"
Command="{Binding SelectPageCommand}"
CommandParameter="{x:Static PageName.SettingsPage}" />
<Button Content="Load Projects"
Command="{Binding SelectPageCommand}"
CommandParameter="{x:Static PageName.ProjectPage}" />
</StackPanel>
<ContentControl Content="{Binding SelectedPage}" />
<StackPanel>
</Window>
The short version:
public class MyViewModel : ViewModel
public MyViewModel()
{
View = new MyUserControlView();
View.DataContext = this; // allow the view to bind to the viewModel.
}
....
public UIElement View {
get; private set;
}
}
And then in XAML:
<ContentControl Content={Binding View} />
There are variations on this theme but that's the basic premise. e.g., if you have a ViewModel that can be bound to multiple views, or ViewModels that have lifetimes longer than their view, you can use a FrameViewModel class like this:
public class FrameViewModel : INotifyProperyChanged; {
public FrameViewModel(IViewModel viewModel; )
{
ViewModel = viewModel;
View = viewModel.CreateView();
View.DataContext = ViewModel;
}
public IViewModel ViewModel { get; set;...}
public UIElement View { get; set; }
}
And then bind THAT into the host XAML with a ContentControl binding to Frame.View.
A more pure approach is to the use the DataTemplateSelector class to instantiate the User Control in a DataTemplate. This is probably the method that WPF designers had in mind for connecting View and ViewModel in WPF. But it ends up spreading the mapping of View and ViewModel across three separate files (the custom C# DataTemplateSelector implementation; widely-separated static resource declaration and ContentControl wrapper in the hosting Window/Page; and the DataTemplate resources themselves which end up in resource files eventually if you have anything but a trivial number of ViewModel/View bindings.
Purists would argue, I suppose, that there's something dirty about having a viewmodel create a view. But there's something far more dirty about code to make DataTemplateSelectors work spread across five files, and inevitable complications with databindings that ensue while trying to tunnel a binding through a DataTemplate.

Populate ListView upon changing combobox - proper MVVM implementation

My goal is: ComboxBox with list of Jira account names and ListView that will display response from query sent to jira upong selecting the user in ComboBox (because account name is the part of the query).
What I have: little knowledge of C#, WPF, MVVM and working solution (code below), but it's not MVVM in any way. So, I've read a lot of about MVVM (relayCommand, PropertyChanged, etc), but for some reason I just can't come up with the solution on how to refactor this program to MVVM. One of the biggest problem is that I cant figure out how make that request to Jira and result in a form of IQueryable fit the MVVM-pattern. I mean, where should I place it.
So, please, if anyone could hint me what should I do generally to convert this program to follow MVVM pattern or any other type of advice, I would be very grateful!
MainWindow.xamls.cs
public ObservableCollection<Issue> Issues { get; set; }
private void OnNameComboChanged(object sender, EventArgs e)
{
Issues.Clear();
string name = ((sender as ComboBox).SelectedItem as ComboBoxItem).Content as string;
Issues fetchedIssues = new Issues();
var issuesList = fetchedIssues.FetchIssues(name); // returns the list of Issues in a type of --> IQueryable<Issue>
foreach (var issue in issuesList)
{
Issues.Add(issue);
}
}
public MainWindow()
{
Issues = new ObservableCollection<Issue>();
InitializeComponent();
}
MainWindow.xaml
<Controls:MetroWindow x:Name="Main_Window" x:Class="Dull.MainWindow"
........
DataContext="{Binding RelativeSource={RelativeSource Self}}"> <!-- how I link contexts-->
<Controls:MetroWindow.RightWindowCommands>
<Controls:WindowCommands>
<ComboBox x:Name="Name" SelectionChanged="OnNameComboChanged" > <!-- Combox box with nicknames -->
<ComboBoxItem>name of the user</ComboBoxItem>
<ComboBoxItem>another name of the user</ComboBoxItem>
</ComboBox>
</Controls:WindowCommands>
</Controls:MetroWindow.RightWindowCommands>
<Grid>
<ListView x:Name="issuesListView" ItemsSource="{Binding Issues}"> <!-- ListView binded to Issues collection -->
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Summary}"
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
There are various frameworks like Prism, Caliburn Micro, MVVMLight and many more which provide the features to write MVVM design pattern application. Few of the features which the mentioned frameworks provides
DelegateCommand or RelayCommand
ViewModelLocator
Container/Module
Event Aggregator
These features ease to write the code in MVVM design pattern. However if you don't require all these features and don't want to integrate those then don't worry about it.
Now, all the conversation in this answer is based on that you want to write for your implementation without these frameworks.
You can refer to this blog to write RelayCommand. You do require ICommand implementation if you want to segregate View from ViewModel. These commands of ViewModel can be integrated with View using Blends' Interactivity trigger (refer this sample).
All above was a pre-work for the solution of your question. Follow the steps
Create a ViewModel
The below ViewModel depicts what you require:
public class MyViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private ObservableCollection<Issue> issues = new ObservableCollection<Issue>();
public ObservableCollection<Issue> Issues { get {return issues;} }
private ObservableCollection<string> users = new ObservableCollection<string>();
public ObservableCollection<string> Users { get {return users;} }
private string user;
public string User
{
get
{
return user;
}
set
{
user = value;
NotifyPropertyChanged();
}
}
private ICommand userChangedCommand;
public ICommand UserChangedCommand
{
get
{
return userChangedCommand ?? (userChangedCommand = new RelayCommand(
x =>
{
OnUserChanged();
}));
}
}
private ICommand loadedCommand;
public ICommand LoadedCommand
{
get
{
return loadedCommand?? (loadedCommand= new RelayCommand(
x =>
{
// Write Code here to populate Users collection.
}));
}
}
private void OnUserChanged()
{
Issues.Clear();
string name = this.User;
Issues fetchedIssues = new Issues();
var issuesList = fetchedIssues.FetchIssues(name); // returns the list of Issues in a type of --> IQueryable<Issue>
foreach (var issue in issuesList)
{
Issues.Add(issue);
}
}
private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
2. View Changes:
<Controls:MetroWindow x:Name="Main_Window" x:Class="Dull.MainWindow" ........
>
<i:EventTrigger EventName="Loaded" >
<i:InvokeCommandAction Command="{Binding LoadedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
<Controls:MetroWindow.RightWindowCommands>
<Controls:WindowCommands>
<ComboBox x:Name="Name" ItemsSource="{Binding Users}" SelectionChanged="OnNameComboChanged" SelectedItem="{Binding User}" > <!-- Combox box is getting user details from ViewModel -->
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged" >
<i:InvokeCommandAction Command="{Binding UserChangedCommand}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ComboBox>
</Controls:WindowCommands>
</Controls:MetroWindow.RightWindowCommands>
<Grid>
<ListView x:Name="issuesListView" ItemsSource="{Binding Issues}"> <!-- ListView binded to Issues collection -->
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Summary}"
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
3. Now the last part How to Bind ViewModel to View.
If you are using the mentioned frameworks then this would be trivial based on ViewModelLocator feature. However, to achieve without the frameworks, you can use one of the below approach.
1) Create the instance ViewModel and assign in the Control's InitializeComponent method (.Xaml.cs)
var vm = new MyViewModel();
this.DataContext = vm;
However this breaks the pure MVVM design pattern
2) You can create the instance in View itself
<Controls:MetroWindow x:Name="Main_Window" x:Class="Dull.MainWindow">
<Controls:MetroWindow.DataContext>
<VM:MyViewModel />
</Controls:MetroWindow.DataContext>
...............
</Controls:MetroWindow>

Set frame content when a button is clicked using MVVM

I'm new to the MVVM pattern, but I understand some of it. The problem I currently have is that I want to open a page when a button is pressed, using the MVVM pattern. When one of the six buttons is pressed, a command can give me the name of the button that is pressed. My problem is that I don't know how to set the frame's content when the button is pressed.
<StackPanel>
<Button Content="Page 1" x:Name="Page1"
Command="{Binding SimpleCommand, Source={StaticResource ViewModelBase}}"
CommandParameter="{Binding Name, ElementName=Page1}"/>
<Button Content="Page 2" x:Name="Page2"
Command="{Binding SimpleCommand, Source={StaticResource ViewModelBase}}"
CommandParameter="{Binding Name, ElementName=Page2}"/>
</StackPanel>
Above is the XAML code right now. The simplecommand is just to write out the name on the button
<Frame x:Name="MainFrame" Grid.Column="1" Grid.Row="1"
Content="{Binding Name, Converter={StaticResource Converter}, ElementName=Page1}"
NavigationUIVisibility="Hidden"/>
Above is the Frame that i want to change the content. On compile time i can set the page that it should open. I want to set the content on run time, where i use the button name.
The converter is just the IValueConverter, where i set what page it should display.
The way I have approached this was not by using a frame but using a ContentPresenter. You can always insert the ContentPresenter inside of your Frame. Bear in mind that Frame doesn't inherit DataContext so I would avoid using it.
To start of let's create a BaseViewModel be our starting point for views.
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Now that we have the base let's create a MainViewModel:
public class MainViewModel : BaseViewModel
{
private BaseViewModel selectedViewModle;
public BaseViewModel SelectedViewModel
{
get { return selectedViewModle; }
set { selectedViewModle = value; OnPropertyChanged(nameof(SelectedViewModel)); }
}
}
At this point our MainViewModel has a property with SelectedViewModel this is what we are going to use for our navigation.
Assumption
I am assuming that you have a working knowledge about commands and how to use them.
Here is a code example of a method for your Navigate command:
void navigate(object parameter)
{
SelectedViewModel = new DetailsViewModel();
}
And here is the code for DetailsViewModel:
public class DetailsViewModel : BaseViewModel
{
//your code with properties and methods here
}
Now let's set up the view:
<UserControl ...>
<Grid>
<ContentPresenter Content="{Binding .}"/>
</Grid>
</UserControl>
Now in the Resources tag for your UserControl include a DataTemplate:
<DataTemplate DataType="{x:Type vm:DetailsViewModel}">
<Grid .../>
</DataTemplate>
At this point you will have the content of the data template presented for you on the screen.

MVVM Light: How to get data from usercontrols?

I have a class MyDataCollection that contains MyMetaData and MyData. In my application i have two usercontrolls that display input fields to the user. One for the MyMetaData and the other for MyData. Both usercontrols are included in the MainPage.
My Question is: How should i get the data from the usercontrols then the user klicks on the save-button (located on the mainpage)?
Update
I have changed my code accordingly to blindmeis post but now the MetaDataView is not shown:
<UserControl.Resources>
<DataTemplate x:Key="MetaDataTemplate">
<view:MetaDataView/>
</DataTemplate>
</UserControl.Resources>
<Grid>
<ContentPresenter Content="{Binding MetaDataTemplate}"/>
</Grid>
why not doing mvvm the easy way?(viewmodel first). you say you have a mainpage - this means you also have a mainpageviewmodel. your mainpageviewmodel handles at least the save command. now you want to show MyMetaData and MyData on your mainpage. so the easy way would be to create one MyMetaData instance and one MyData instance in your mainviewmodel.
public class MainPageViewmodel
{
public ICommand SaveCommand { get; set; }
public MyDataViewmodel MyData { get; set; }
public MyMetaDataViewmodel MyMetaData { get; set; }
public MainPageViewmodel()
{
this.MyData = new MyDataViewmodel();
this.MyMetaData = new MyMetaDataViewmodel();
}
}
public class MyDataViewmodel
{}
public class MyMetaDataViewmodel
{}
your mainpage just need 2 datatemplates and 2 contentpresenter.
//resources
<DataTemplate DataType="{x:Type Local:MyDataViewmodel}">
<view:MyDataUserControl/>
</DataTemplate>
<DataTemplate DataType="{x:Type Local:MyMetaDataViewmodel}">
<view:MyMetaDataUserControl/>
</DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<ContentPresenter Content="{Binding MyData}" Grid.Column="0"/>
<ContentPresenter Content="{Binding MyMetaData}" Grid.Column="1"/>
<Button Content="Save" Command="{Binding SaveCommand}" Grid.Column="2"/>
</Grid>
because your mainpageviewmodel has both "child" viewmodel, you have all information you want on your savecommand.
if you have another scenario pls update your question, maybe post some code.
EDIT: i have no silverlight so that just a suggestion: maybe rachel can give you a better answer.
<Grid>
<ContentPresenter Content="{Binding MyMetaData}" ContentTemplate="{StaticResource MetaDataTemplate}"/>
</Grid>
if silverlight cant handle datatemplates with datatype you could just put the usercontrol there directly.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<view:MyDataUserControl DataContext="{Binding MyData}" Grid.Column="0"/>
<view:MyMetaDataUserControl DataContext="{Binding MyMetaData}" Grid.Column="1"/>
<Button Content="Save" Command="{Binding SaveCommand}" Grid.Column="2"/>
</Grid>
Since you tagged this question as MVVM, your ViewModels should contain both your SaveCommand and all the data needed to perform the actual save
Your MainViewModel should contain MyMetaData and MyData properties (which are bound to their respective UserControls), and each of those objects should contain properties for any data needed in the UserControl. For example, if your UserControl had a TextBox for Name, then your data object should have a property for the Name that the TextBox binds to.
If the Save button is located in one of those UserControls then the respective ViewModel should have a SaveCommand that gets executed when the Button is clicked. All the data needed for the Save is also located in that ViewModel, so you're good to go.
If your MainViewModel is in charge of saving the data, then it should be able to hook into your sub ViewModel's SaveCommand and attach it's own method, such as
this.MyData.SaveCommand = this.SaveCommand();
and all the data needed for the save can be found in this.MyData
If the SaveButton is located in your MainView, and not in one of the UserControls, then the SaveCommand should be part of MainViewModel, and all the data needed for the save can be found in this.MyData or This.MyMetaData.
Remember, with MVVM your ViewModels are your application. The View is just a pretty interface that allows users to interact with your ViewModels.
You should use Two-way bindings to automatically update the value in your controller. Take a look at this article.
Here's an example:
<TextBox Text="{Binding MyMetaData, Mode=TwoWay }" />
<TextBox Text="{Binding MyData, Mode=TwoWay }" />
I'll give you a little sample, how you can use the MVVM Light Messenger for ViewModel-to-ViewModel communication. Say you have an MyDataCollection class:
public class MyDataCollection
{
public int MyData;
public string MyMetaData;
}
On your MainViewModel you have a RelayCommand (from MVVM light toolkit) binded by your View's SaveButton. When the Connad is executed, you will have to send a Message with a callback action to request data from the subcriber. The callback takes MyDataCollection as parameter:
public class MainViewModel : ViewModelBase
{
public RelayCommand SaveCommand { get; private set; }
//Ctor
public MainViewModel()
{
SaveCommand = new RelayCommand(
() =>
Messenger.Default.Send<NotificationMessageAction<MyDataCollection>>(
new NotificationMessageAction<MyDataCollection>("SaveData", SaveCallback)));
}
private void SaveCallback(MyDataCollection dataCollection)
{
// process your dataCollection...
}
}
The UserControlViewModel has properties the InputTextBoxes are binded too. It just has to register to the message and call the callback with data properties:
public class UserControlViewModel : ViewModelBase
{
//Properties
public string UserControlMetaData { get; set; }
public int UserControlData { get; set; }
//Ctor
public UserControlViewModel()
{
Messenger.Default.Register<NotificationMessageAction<MyDataCollection>>(this, MessageReceived);
}
// private Method to handle all kinds of messages.
private void MessageReceived(MessageBase msg)
{
if(msg is NotificationMessageAction<MyDataCollection>)
{
var actionMsg = msg as NotificationMessageAction<MyDataCollection>;
if(actionMsg.Notification == "SaveData") // Is this the Message, we are looking for?
{
// here the MainViewModels callback is called.
actionMsg.Execute(new MyDataCollection() {MyData = UserControlData, MyMetaData = UserControlMetaData});
}
}
}
}
You will have to use messengers or you will have to set the properties over the ViewModelLocator
Messenger example of how I use it to set the UI language
ViewModel A, I register a listener here with the "SetLanguage" token:
Messenger.Default.Register<string>(this, "SetLanguage", false, input =>
{
SetLanguage(input);
});
ViewModel B, here I send the message with the "SetLanguage" token:
Messenger.Default.Send("en-EN", "SetLanguage");
ViewModelLocator example in ViewModel A, I access data in ViewModel B over the locator:
short value = 12;
var myFilteredDataList = ViewModelLocator.ViewModelBStatic.MyDataList.Any(m => m.code == value);
I have two solutions now:
View:
<ContentPresenter Content="{Binding MyMetaDataView}" />
ViewModel:
public MetaDataViewModel MyMetaDataViewModel { get; set; }
public MetaDataView MyMetaDataView { get; set; }
public MainViewModel()
{
MyMetaDataViewModel = new MetaDataViewModel();
MyMetaDataView = new MetaDataView();
MyMetaDataView.DataContext = MyMetaDataViewModel;
}
or ----
View:
<UserControl.Resources>
<DataTemplate x:Key="MetaDataViewTemplate">
<view:MetaDataView />
</DataTemplate>
</UserControl.Resources>
...
<ContentPresenter Content="{Binding MyMetaDataViewModel}" ContentTemplate="{StaticResource MetaDataViewTemplate}"/>
ViewModel:
public MetaDataViewModel MyMetaDataViewModel { get; set; }
public MainViewModel()
{
MyMetaDataViewModel = new MetaDataViewModel();
}

Categories