Create new ViewB + ViewmodelB in other ViewModelA and show them in ViewA - c#

I use MVVM. I have a ViewA with a Grid in xaml and a ViewModelA. In the ViewModelA I have a Method that looks like this which is called on a Button click:
public void ButtonClickMethod()
{
ViewB viewB = new ViewB();
viewB.DataContext = new ViewBViewModel();
}
How do I add all the created viewBs into the Grid on my ViewA, so I can see them there? Or is my solution wrong in general maybe?
EDIT :
I now used a ObservableCollection<ViewB> in my ViewModelA which notifies on changes in my ViewA xaml at
<ItemsControl ItemsSource="{Binding ObservableCollection<ViewB>}" />
The only problem is the ViewBs shown in the ItemControl should be dragable. Therefore I used expression blend's
Interaction.GetBehaviors(ViewB).Add(new MouseDragElementBehavior() { });
when i create a new ViewB. But it doesn't work.
EDIT2 :
I tried this solution but it doesn't work for me :( Using MouseDragElementBehavior with an ItemsControl and Canvas

Add your Grid an ContentControl and bind it content to your second view.
Here an example with an UserControl.
<ContentControl Grid.Column="0" Margin="5" Content="{Binding SecondView}"/>
Code im ViewModel:
private UserControl secondView;
public UserControl SecondView
{
get
{
return secondView;
}
set
{
this.SetProperty(ref secondView,value);
}
}

Related

Handling Events in MVVM

So this is my first MVVM application. I have a "shell" view model named MainWindowViewModel for the main window that basically splits the view into two pages: MainWindowRibbon and MainWindowFrame. The MainWindowViewModel stores both pages as properties, which I plan to use databinding to update in the UI. Here is some of the code for reference:
MainWindowView xaml~
<Grid>
<Frame Content="{Binding MainWindowRibbon}" Grid.Column="0" Grid.Row="0"/>
<ScrollViewer>
<Frame Content="{Binding MainWindowFrame}"/>
</ScrollViewer>
</Grid>
MainWindowView code behind~
public partial class MainWindowView : Window
{
public MainWindowView()
{
InitializeComponent();
mainWindowViewModel = new MainWindowViewModel();
DataContext = mainWindowViewModel;
}
public MainWindowViewModel mainWindowViewModel;
}
MainWindowViewModel code~
public MainWindowViewModel()
{
//MainWindowRibbon and MainWindowFrame are declared as public Page properties
MainWindowRibbon = new MainWindowRibbonView();
MainWindowFrame = new WelcomePageView();
}
The MainWindowRibbonView, like the MainWindowView, instantiates the MainWindowRibbonViewModel.
My trouble comes when I wish to use an event within the MainWindowRibbonViewModel that will call for the MainWindowViewModel to reassign the MainWindowFrame page. I do not know how to connect the button command of the navigation bar I have created in the MainWindowRibbonView to cause an event or change in the MainWindowViewModel.
I do not know if the way I have organized this is ideal. Please let me know if I need to revise.
If somebody could help me determine the best approach, or even just a functioning one, I would be very grateful.
P.S.
Sorry if the naming conventions aren't the greatest.
Edit:
Lesson learned: listen to Joe.
I suppose it depends on what kind of button you are using in your navigation bar. is it a RadioButton? A RibbonToggleButton? Is it a regular button binding to an ICommand?
Since you called your Navigation Bar a "Ribbon", let us suppose it is a RibbonToggleButton (which is still basically a CheckBox). If it is checked, you show some view-model your "page 1". If it is not checked, you should another view-model representing your "page 2"
Let us also suppose your view's ribbon is up top. So you have two rows: the Ribbon row and the content row.
I might rewrite your MainWindow to look like this, (note the IsChecked property to some boolean property in your view-model like so:)
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- The "Ribbon" -->
<RowDefinition Height="*"/> <!-- The page content -->
</Grid.RowDefinitions>
<ToggleButton Content="Show Page 1" IsChecked="{Binding ShowPage1}"/>
<ScrollViewer Grid.Row=1>
<Frame Content="{Binding CurrentViewModel}"/>
</ScrollViewer>
</Grid>
And I might write your view-model like this: (Note that I assume it implements INotifyPropertyChanged and I call a function RaisePropertyChanged that I do not show.
public class Page1ViewModel {} // Fill this out with Page 1 properties
public class Page2ViewModel {} // Fill this out with Page 2 properties
// MainWindowViewModel. Implements INotifyPropertyChanged. Implementation
// is not shown here.
public class MainWindowViewModel : INotifyPropertyChanged
{
private Page1ViewModel = new Page1ViewModel();
private Page2ViewModel = new Page2ViewModel();
public MainWindowViewModel()
{
_currentViewModel = Page1ViewModel;
ShowPage1 = true;
}
private object _currentViewModel;
// The current contents of the main frame.
public object CurrentViewModel
{
get => _currentViewModel;
set
{
if (value == _currentViewModel)
return;
_currentViewModel = value;
RaisePropertyChanged();
}
// Should CurrentViewModel be page 1 or page 2?
public bool ShowPage1
{
get => return _currentViewModel == Page1ViewModel;
set
{
if (value == ShowPage1)
return;
CurrentViewModel = value ? Page1ViewModel : Page2ViewModel;
RaisePropertyChanged();
}
}
}
Note that I am not showing you any of the properties of Page1VieModel or Page2ViewModel nor have I shown you the implicit DataTemplates I assume you will write for them.
Also I am assuming that your navigation bar (and MainWindowView in general) have a DataContext that is already set to the MainWindowViewModel
The implemention with a command button or a RadioButton would be quite different.

Pass parameter via shell when navigating in UWP application using MVVM

Have started learning UWP development by creating a Windows Template Studio application with Horizontal Navigation project type, MVVM Light design pattern and two blank pages (MainPage and StatsPage).
In the MainPage I have a list of items and I want to show the selected item's statistics in the StatsPage when the user hits the relevant NavigationViewItem. In my wpf apps, I would set up a RelayCommand with command parameter on a button in the Main view, and in the viewmodel, call the command's method with the param/arg to open the Stats view with the correct info. In the created Template Studio app, the NavigationViewItem in the ShellPage calls OnItemInvoked() in its VM which doesn't know about the selection in the MainPage view.
<winui:NavigationView.MenuItems>
<winui:NavigationViewItem x:Uid="Shell_Main" helpers:NavHelper.NavigateTo="LG_Ess.ViewModels.MainViewModel" />
<winui:NavigationViewItem x:Uid="Shell_Stats" helpers:NavHelper.NavigateTo="LG_Ess.ViewModels.StatsViewModel" />
</winui:NavigationView.MenuItems>
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="ItemInvoked">
<ic:InvokeCommandAction Command="{x:Bind ViewModel.ItemInvokedCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
How do I pass the MainPage selected item as a param to StatsPage via the ShellPage's NavigationViewItem? I could probably work it out by adding a button to my MainPage and hiding the Shell navigation, but I'd prefer to do it in the Template Studio style.
Further investigations show that the ShellViewModel seems to be the default DataContext for all pages created by the Template Studio. In my pages I have set the Data Context to the autogenerated View Models by adding:
<Page.DataContext>
<local:{PageName}ViewModel/>
</Page.DataContext>
So, it seems I can either have a single ViewModel and treat the pages as UserControls, or map the DataContext for each page to its own ViewModel, and do some ViewModelLocator dancing to access properties from one VM in another.
So, it seems I can either have a single ViewModel and treat the pages as UserControls, or map the DataContext for each page to its own ViewModel, and do some ViewModelLocator dancing to access properties from one VM in another.
Correct. You could define a 'SelectedStats' relevant property in your MainViewModel and make your ListView's SelectedItem bind to this property and set Mode=TwoWay. Then, on your 'StatsPage', you could use EventTriggerBehavior to bind a 'LoadedCommand' in StatsViewModel like the 'ShellPage'. In StatsViewModel, you could get the selected item by calling ViewModelLocator.Current.MainViewModel.SelectedStats.
See my simple code sample:
<!--MainPage.xaml-->
<ListView ItemsSource="{x:Bind ViewModel.list}" SelectedItem="{x:Bind ViewModel.SelectedStats,Mode=TwoWay}">
</ListView>
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
list = new ObservableCollection<string>();
list.Add("string1");
list.Add("string2");
list.Add("string3");
}
public ObservableCollection<string> list { get; set; }
private object _SelectedStats;
public object SelectedStats
{
get { return _SelectedStats; }
set
{
if (_SelectedStats != value)
{
_SelectedStats = value;
RaisePropertyChanged("SelectedStats");
}
}
}
}
<!--StatsPage.xaml-->
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="Loaded">
<ic:InvokeCommandAction Command="{x:Bind ViewModel.LoadedCommand}" />
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
public class StatsViewModel : ViewModelBase
{
public StatsViewModel()
{
}
private ICommand _LoadedCommand;
public ICommand LoadedCommand => _LoadedCommand ?? (_LoadedCommand = new RelayCommand(LoaedAsync));
private async void LoaedAsync()
{
var selectedObject = ViewModelLocator.Current.MainViewModel.SelectedStats;
//TODO:...
await Task.CompletedTask;
}
}

WPF UserControl property binding

I'm trying to create a WPF UserControl which contains 2 buttons. I use this UserControl in a Window and apply a Window.Resource value to set the background of one button inside the user control.
Currently I have:
window.xaml
<Window.Resources>
<SolidColorBrush Color="Brown" x:Key="theBG"></SolidColorBrush>
</Window.Resources>
<theControl:TheControl
x:Name="TheControl"
buttonBG="{Binding Source={StaticResource theBG}}" />
usercontrol.xaml.cs
public SolidColorBrush buttonBG
{
get { return base.GetValue(buttonBGProperty) as SolidColorBrush; }
set { base.SetValue(buttonBGProperty, value); }
}
public static readonly DependencyProperty buttonBGProperty = DependencyProperty.Register("buttonBG", typeof(SolidColorBrush), typeof(DataPanel), null);
usercontrol.xaml
<Button ... Background="{Binding buttonBG}">
I was expecting this to work but the background is not the one I set in the window resource.
What am I doing wrong?
Background="{Binding buttonBG}"
Implies either that you changed the DataContext of the UserControl, which you should never do. Or that the binding is just wrong.
Use
Background="{Binding buttonBG, ElementName=control}"
Naming your UserControl root element control. RelativeSource works as well.
Try placing it in a separate model or even a viewmodel that has INotifyPropertyChanged. When you add view code in the cs for an xaml file, you need to bind with relativesource self and its hacky and goes against MVVM. I would create a seperate ViewModel with a Brush that has NotifyPropertyChanged baked into it. This will tell the UI to change everything its bound to on value change.
In the Window, bind your viewmodel to datacontext. In the viewmodel you can put:
private Brush _bgColor;
public Brush BgColor
{
get{return _bgColor;
}
set
{
_bgColor = value;
OnPropertyChanged("BgColor");
}
Create an ICommand, and bind your button to it like this in the viewmodel:
ICommand ChangeBgColor {get;set;
And in the XAML for the Button:
Command="{Binding Path=DataContext.ChangeBgColor,RelativeSource={RelativeSorce Mode=FindAncestor,AncestorType={x:Type UserControl}}"
This will fire the ICommand, bound to the viewmodel that is the datacontex of the window that you are working with.
And in that code for the ICommand change out your colors, you could do it like this:
private void OnChangeBgColor(object param){
var bc = new BrushConverter();
BgColor = (Brush)bc.ConvertFrom("#fff");
}
With the MVVM pattern, you want to get away from putting unnecessary code in the xaml.cs files and start putting them into viewmodels and models.

RelayCommand not getting the right Model

I created a user control that looks like a tile. Created another user control named TilePanel that serves as the default container of the tiles. And lastly, the very UI that looks like a Window start screen. I used RelayCommand to bind my TileCommands
Here are the codes:
Tilev2.xaml
<UserControl x:Class="MyNamespace.Tilev2"
Name="Tile"....
>
...
<Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" Command="{Binding ElementName=Tile, Path=TileClickCommand}" >
</Button>
</UserControl>
Tilev2.xaml.cs
public partial class Tilev2 : UserControl
{
public Tilev2()
{
InitializeComponent();
}
//other DPs here
public ICommand TileClickCommand
{
get { return (ICommand)GetValue(TileClickCommandProperty); }
set { SetValue(TileClickCommandProperty, value); }
}
// Using a DependencyProperty as the backing store for TileClickCommand. This enables animation, styling, binding, etc...
public static readonly DependencyProperty TileClickCommandProperty =
DependencyProperty.Register("TileClickCommand", typeof(ICommand), typeof(Tilev2));
}
}
Then I created a TilePanel user control as the container of the tiles
TilePanel.xaml
<UserControl x:Class="MyNamespace.TilePanel"
...
>
<Grid>
<ScrollViewer>
<ItemsControl Name="tileGroup"
ItemsSource="{Binding TileModels}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<local2:Tilev2 TileText="{Binding Text}"
TileIcon="{Binding Icon}"
TileSize="{Binding Size}"
TileFontSize="{Binding FontSize}"
Background="{Binding Background}"
TileCaption="{Binding TileCaption}"
TileCaptionFontSize="{Binding TileCaptionFontSize}"
TileClickCommand="{Binding TileCommand}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>
TilePanel.xaml.cs
public partial class TilePanel : UserControl
{
public TilePanel()
{
InitializeComponent();
DataContext = new TilePanelViewModel();
}
public TilePanelViewModel ViewModel
{
get { return (TilePanelViewModel)this.DataContext; }
}
}
My ViewModel for TilePanel
TilePanelViewModel.cs
public class TilePanelViewModel : ViewModelBase
{
private ObservableCollection _tileModels;
public ObservableCollection<TileModel> TileModels
{
get
{
if (_tileModels == null)
_tileModels = new ObservableCollection<TileModel>();
return _tileModels;
}
}
}
Then my Tile model
TileModel.cs
public class TileModel : BaseNotifyPropertyChanged
{
//other members here
ICommand tileCommand { get; set; }
//other properties here
public ICommand TileCommand
{
get { return tileCommand; }
set { tileCommand = value; NotifyPropertyChanged("TileCommand"); }
}
}
}
This is my StartScreen View where TilePanels with tiles should be displayed...
StartScreen.xaml
<UserControl x:Class="MyNamespace.StartMenu"
... >
<Grid>
<DockPanel x:Name="dockPanel1" Grid.Column="0" Grid.Row="1" Margin="50,5,2,5">
<local:TilePanel x:Name="tilePanel"></local:TilePanel>
</DockPanel>
</Grid>
</UserControl>
StartScreen.xaml.cs
public partial class WincollectStartMenu : UserControl, IView<StartMenuViewModel>
{
public WincollectStartMenu()
{
InitializeComponent();
}
public StartMenuViewModel ViewModel { get { return (DataContext as StartMenuViewModel); } }
private void UserControl_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
ViewModel.Tile = tilePanel.ViewModel.TileModels;
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
return;
}
}
In my start screen ViewModel, I used ObservableCollection Tile
and use Tile.Add(tile); to populate my start screen with Tiles inside the TilePanel...
StartMenuViewModel.cs
TileModel tile = new TileModel() { Text = "Testing1", FontSize = 11, Size = TileSize.Medium, Background = (SolidColorBrush)new BrushConverter().ConvertFromString("#039BE5"), Tag="Something" };
tile.TileCommand = new RelayCommand(
p => Tile_TileClick(tile.Tag),
p => true
);
temp.Add(tile);
Now the problem is, if I add a new code below, tile = new TileModel() {...}
tile.TileCommand = new RelayCommand(...), even if I clicked on the first tile, my Tile_TileClick() will get the second tile's info (or the last tile inserted)...
Am I doing something wrong? Or Im doing everything wrong...?
This is not direct answer to your question, but hopefully it will give you few thoughts.
Ok, first of all, don't name your usercontrol like this:
<UserControl x:Class="MyNamespace.Tilev2" Name="Tile"/>
because the name can be easily overriden when using the usercontrol somewhere:
<local:Titlev2 Name="SomeOtherName" />
and the binding inside Tilevs with ElementName won't work: Command="{Binding ElementName=Tile, Path=TileClickCommand}"
Second, what's the point of Tilev2 usercontrol? Why don't just put the button directly to the DataTemplate inside TilePanel class?
If you need to reuse the template, you can put the template to resource dictionary.
If you need some special presentation code in the Tilev2 codebehind or you need to use the Tilev2 without viewmodel, it's better to create custom control instead of usercontrol in this case. it has much better design time support, and writing control templates it's easier (Triggers, DataTriggers, TempalteBinding, etc). If you used custom Control insead UserControl, you wouldn't have to write {Binding ElementName=Tile, Path=TileClickCommand}, or use RelativeSource, etc.
Third, it seems like you forced MVVM pattern where you can't really take advantage of it. Point of MVVM is separate application logic from presentation. But your Tile and TilePanel usercontrols are just presentation. You application logic could be in StartScreen which is concrete usage of TileName.
I would create custom controls called TilePanel (potentionally inherited from ItemsControl, Selector or ListBox) and if needed also for Tile. Both controls should not be aware of any viewmodels. There's absolutelly no need for that.
Take ListBox as an example. ListBox does not have viewmodel but can be easily used in MVVM scenarios. Just because ListBox it is not tied to any viewmodel, it can be databound to anything.
Just like ListBox creates ListBoxItems, or
Combobox creates ComboBoxItems, or
DataGrid creates DataGridRows or
GridView (in WinRT) creates GridViewRow, your TilePanel could create Tiles.
Bindings to tile specific properties, like Icon or Command could be specified in TilePanel.ItemContainerStyle orusing simillar appriach like DisplayMemberPath, resp ValueMemberPath in ListBox.
final usage could the look like:
<TilePanel ItemsSource="{Bidning ApplicationTiles}" />
or
<TilePanel>
<Tile Icon=".." Command=".." Text=".." />
<Tile Icon=".." Command=".." Text=".." />
</TilePanel>
Last, the name `TilePanel' evoked that it is some kind of panel like StackPanel, WrapPanel, etc. In other words, it is FrameworkElement inherited from Panel.
TilesView would be more suitable name for the control than TilePanel. The -View postfix is not from MVVM, it just follows naming convention -GridView, ListView...
Saw the problem...
To pass a parameter from button, I used CommandParameter so I could use it in switch-case scenario to know which button was clicked. But still, param was still null...
<Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" CommandParameter="{Binding}" Command="{Binding Path=TileClickCommand, ElementName=Tile}" >
</Button>
TileCommand = new MyCommand() { CanExecuteFunc = param => CanExecuteCommand(), ExecuteFunc = param => Tile_TileClick(param)}
After 2 whole damn days, I changed it:
From this:
<UserControl Name="Tile"...>
<Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" CommandParameter="{Binding Tag, ElementName=Tile}" Command="{Binding Path=TileClickCommand, ElementName=Tile}" >
</Button>
</UserControl>
To this:
<UserControl Name="Tile"...>
<Button x:Name="btnTile" Style="{StaticResource TileStyleButton}" CommandParameter="{Binding}" Command="{Binding Path=TileClickCommand, ElementName=Tile}" >
</Button>
</UserControl>
My first post does error because CommandParameter does not know where to get its DataContext so I replaced it to CommandParameter={Binding} so it will get whatever from the DataContext.

UserControl DataContext Binding

I have three projects in my solution:
My main WPF Application which contains a MainWindow + MainViewModel
UserControl Library with a UserControl (ConfigEditorView)
UIProcess class with the ViewModel for the UserControl (ConfigEditorViewModel)
In my MainWindow I want to use the UserControl with the ViewModel of UIProcess.
First I set the UserControl in my MainWindow:
<TabItem Header="Editor">
<Grid>
<cel:ConfigEditorView DataContext="{Binding ConfEditModel, NotifyOnSourceUpdated=True, NotifyOnTargetUpdated=True, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
</TabItem>
I don't know which of these properties I need here, so I put all together but it still doesn't work.
Then I've set this in my MainViewModel:
public ConfigEditorViewModel ConfEditModel { get; set; }
With simple method that is bound to a Button:
private void doSomething()
{
ConfEditModel = new ConfigEditorViewModel("Hello World");
}
My ConfigEditorViewModel looks basically like this:
public class ConfigEditorViewModel : ViewModelBase
{
private string _Description;
public string Description
{
get
{
return _Description;
}
set
{
_Description = value;
base.RaisePropertyChanged();
}
}
public ConfigEditorViewModel(string t)
{
Description = t;
}
}
The description is bound to a TextBox in my UserControl.
<TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,0,10" Text="{Binding Description}"/>
When I start the application and click the Button the TextBox should contain "Hello World" but it's empty.
What I've done wrong?
i gave you a general answer:
within a "real(a usercontrol you wanna use with different viewmodels with different property names)" usercontrol you bind just to your own DependencyProperties and you do that with ElementName or RelativeSource binding and you should never set the DataContext within a UserControl.
<UserControl x:Name="myRealUC" x:class="MyUserControl">
<TextBox Text="{Binding ElementName=myRealUC, Path=MyOwnDPIDeclaredInMyUc, Path=TwoWay}"/>
<UserControl>
if you do that you can easily use this Usercontrol in any view like:
<myControls:MyUserControl MyOwnDPIDeclaredInMyUc="{Binding MyPropertyInMyViewmodel}"/>
and for completeness: the Dependency Property
public readonly static DependencyProperty MyOwnDPIDeclaredInMyUcProperty = DependencyProperty.Register(
"MyOwnDPIDeclaredInMyUc", typeof(string), typeof(MyUserControl), new PropertyMetadata(""));
public bool MyOwnDPIDeclaredInMyUc
{
get { return (string)GetValue(MyOwnDPIDeclaredInMyUcProperty); }
set { SetValue(MyOwnDPIDeclaredInMyUcProperty, value); }
}
Your view models (and, optionally, models) need to implement INotifyPropertyChanged.
Binding's aren't magic. There is no inbuilt mechanism that allows for code to be notified when a plain old property's value changes. You'd have to poll it in order to check to see if a change happened, which would be very bad, performance-wise.
So bindings will look at the objects they are bound against and see if they implement INotifyPropertyChanged and, if so, will subscribe to the PropertyChanged event. That way, when you change a property and fire the event, the binding is notified and updates the UI.
Be warned, you must implement the interface and use it correctly. This example says it's for 2010, but it works fine.

Categories