MVVM Execute Command on ViewModel from other ViewModel - c#

I'm struggling for about 14 days now with a simple task: In database, I have definitions for hardware categories. For example :
HDD
Internal
External
Flash
This list is in database defined like this:
[ID - ParrentID - Name] : 1 - 0 - HDD, 2 - 1 - Internal, 3 - 1 - External, 4 - 1 - Flash.
Through Entity Framework I get these rows into my application. From this flat data I then create structured object which is my DataModel. This model is defined as follows :
public class Category
{
private int _id = -1;
private string _name = "";
private List<Category> _subCategories = null;
// property getters and setters, constructors, and bool HasSubCategories
}
Now, from these I create ViewModel called SubCategoryViewModel to which is binded my TreeView. So, I can view my categories in treeview and with my defined and maintained hierarchy. This works just fine. In SubCategoryViewModel is defined a Command through Attached Behavior for MouseDoubleClick which is also binded to TreeView. So, when user doubleclicks on Item, in SubViewCategoryModel defined method will execute particular code. List of SubCategoryViewModel is nested in HWDocumentViewModel which is a main ViewModel for my window.
What I need now is obvious : When user doubleclicks on item in TreeView, I need to load items from database and show them in ListView. My opinion is, that in HWDocumentViewModel I need to define an collection of Items and load them accordingly to selected category in ListView. But, I don't know how to execute a method on HWDocumentViewModel from SubCategoryViewModel. Because : TreeView is binded to list of SubCategoryViewModel items, so when DoubleClick occurs, the method on SubCategoryViewModel is executed. I'm searching for a way, how to execute a method on main ViewModel (HWDocumentViewModel).
I tried this approach :
I created a property : public static SubCategoryViewModel SelectedCategory on HWDocumentViewModel. When doubleclick occurs, I set this property from SubCategoryViewModel as this. So, in this property is object, which executed doubleclick event delegate. Great, now I have in HWDocumentView model an object, which user selected.
So, I need to load items to ListView. But, will I load them from method in SubCategoryViewModel ? I don't think so. Instead I should load them from Main View Model by creating a ViewModel for them and bind it to ListView, right ? But, how can I from SubCategoryViewModel call a method in HWDocumentViewModel ? Should I write a static method
on a HWDocumentViewModel which will be accessible from SubCategoryViewModel ?
Or is there a way, how to call Command defined on HWDocumentViewModel from SubCategoryViewModel ?
Or generally, did I take a right approach to create a Warehouse-like application in WPF ?
Thanks a lot.
EDIT: XAML for my TreeView looks like this :
<TreeView x:Name="tvCategories" Background="White" ItemsSource="{Binding Categories}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="behaviors:MouseDoubleClick.Command" Value="{Binding MouseDoubleClickCommand}" />
<Setter Property="behaviors:MouseDoubleClick.CommandParameter" Value="{Binding}" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="FontWeight" Value="Bold" />
</Trigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
<TreeView.Resources>
<HierarchicalDataTemplate DataType="{x:Type localvm:SubCategoryViewModel}" ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding CategoryName}" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.Resources>
</TreeView>

I'm not sure I see the problem. You have a tree of subcategories and when one is selected, the appropriate SubCategoryViewModel sets itself as SelectedCategory on the main HWDocumentViewModel. That seems like a reasonable approach.
So why do you need to call a command? Why can't you just load the new list in HWDocumentViewModel in response to a change of its SelectedCategory property (ie in the setter)?
If you really must use a command to invoke the load, then simply keep a reference to your main HWDocumentViewModel in each SubCategoryViewModel, and invoke the command with a simple:
_mainViewModel.LoadCategoryCommand.Execute();

With MVVM and trying to communicate between View and ViewModel or between ViewModels a publisher/Subscriber setup works well or a messaging paradigm like what's found in MVVMLight or Prism. I posted an answer on MVVM Light's messaging setup here
In the message you can send an object that holds any data you would like to send back and forth between the view models.
I highly recomend using a framework when working with mvvm it makes like much easier. MVVM Framework Comparison is a link to an answer that goes through a comparison of some of the major frameworks.

Related

Unit Test that involves some UI element

Should I be writing unit tests like the following?
Code:
public ObservableCollection<DXTabItem> Tabs { get; private set; }
public ICommand CustomersCommand { get; private set; }
CustomersCommand = new DelegateCommand(OpenCustomers);
private void OpenCustomers()
{
var projectService = new ProjectService(Project.FilePath);
var vm = new CustomersViewModel(projectService);
AddTab("Customers", new CustomersView(vm));
}
public void AddTab(string tabName, object content, bool allowHide = true)
{
Tabs.Add(new DXTabItem { Header = tabName, Content = content });
}
Test:
[TestMethod]
public void CustomerCommandAddsTab()
{
_vm.CustomersCommand.Execute(null);
Assert.AreEqual("Customers", _vm.Tabs[1].Header);
}
XAML:
<dx:DXTabControl ItemsSource="{Binding Tabs}" />
I am using the TDD approach, so this is working code, and it passes the tests locally, however on a server CI build it fails this test because the view (CustomersView) has something inside it that doesn't work. So I realized this test, even though its simple is actually breaking MVVM. I am writing UI code inside the ViewModel by referencing DXTabItems and even new'ing up a View.
What is the correct approach for something like this? Should I not write tests like this at all (and rely on automated testing) or should I refactor it somehow so that the ViewModel contains no UI elements, tips on how I should do that would be useful.
Clarification:
The reason for this kind of design is that each Tab contains a different View, for example the Customers Tab contains the CustomersView, yet another Tab would contain something completely different, in data and presentation. So its hard to define a mechanism that will allow for that in MVVM fashion. At least the answer is not trivial.
If DXTabItem is derived from TabItem then this is not MVVM, in MVVM you never access view elements directly in the view model. What you should be doing instead is creating a view model for your tabs (e.g. TabViewModel), change Tabs to be an ObservableCollection<TabViewModel> and bind your tab control's ItemsSource property to that to create the GUI tabs themselves.
As for your CI failing, you shouldn't ever be creating GUI elements (i.e. CustomersView) in unit tests. The only time you'd do that is during integration testing, which is a different kettle of fish. Views should only ever be loosely coupled to the view model though the mechanism of data-binding, you should be able to both run and test your entire application without creating a single view object.
UPDATE: Actually it's very easy...once you know how! :) There are a couple of different ways to achieve what you're trying to do but the two most common approaches are data templates and triggers.
With data templates you rely on the fact that your view models are supposed to represent the logic behind your GUI. If you have a Client tab and an Product tab (say) then those should have corresponding view models i.e. ClientPage and ProductPage. You may wish to create a base class for these (e.g. TabViewModel) in which case your view model collection would be a ObservableCollection<TabViewModel> as I explained above, otherwise just make it a ObservableCollection<object>. Either way you then use data templates to specify which view to create for each tab:
<DataTemplate DataType="{x:Type vm:ClientPage>
<view:ClientView />
</DataTemplate>
<DataTemplate DataType="{x:Type vm:ProductPage>
<view:ProductView />
</DataTemplate>
ListBox and other collection elements will apply these data templates automatically, alternatively you can specify ListBox.ItemTemplate explicitly and use a ContentControl where needed.
The second method is to use data triggers. If your pages are fixed then I find it helps to create an enumeration in your view model layer for reasons I'll explain in a minute:
public enum PageType : int
{
Client,
Product,
... etc ...
}
Back in your XAML you'll want to create a page for each of these, you can do that in your VM if you like although it's such an easy task I usually do it in XAML:
<ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="PageType">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="vm:PageType" />
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
Now you can create a TabControl and bind ItemsSource to this object and a separate tab will appear for each item in your enum:
<TabControl ItemsSource="{Binding Source={StaticResource PageType}}"
SelectedIndex="{Binding CurrentPage, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}"
IsSynchronizedWithCurrentItem="True">
CurrentPage is, of course, a property in your MainViewModel of type PageType
private PageType _CurrentPage;
public PageType CurrentPage
{
get { return _CurrentPage; }
set { _CurrentPage = value; RaisePropertyChanged(); }
}
XAML isn't smart enough to deal with enums so you'll also need the code for EnumToIntConverter which converts between the two:
public class EnumToIntConverter : IValueConverter
{
#region IValueConverter Members
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return (int)value;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return Enum.ToObject(targetType, value);
}
#endregion
}
Using an enum like this might seem like a bit more work than needed but it does mean that your view model code can now set the active page at any time by doing something like `this.CurrentPage = PageType.Client'. This is especially handy in the later stages of an application where you might want have a list of products somewhere else in your application and you want to provide the user with a button (say) which opens up the product page. This provides your entire application with a lot of control over the behaviour of your tabs. Of course it also means you get notification whenever the user changes Tabs (i.e. when this.CurrentPage changes value) which can be useful for loading data on demand to improve the performance of your application...it doesn't matter if you change the order of the pages around in your enum later because your view model code is checking against an enum instead of an integer page number!
The only other thing I haven't shown is how to display the appropriate child content on each of the pages, and like I said this is done with a data trigger in your listbox item style:
<TabControl.Resources>
<Style TargetType="{x:Type TabItem}" BasedOn="{StaticResource {x:Type TabItem}}">
<Style.Triggers>
<!-- Client -->
<DataTrigger Binding="{Binding}" Value="{x:Static vm:PageType.Client}">
<Setter Property="Header" Value="Client" />
<Setter Property="Content">
<Setter.Value>
<view:ClientView DataContext="{Binding ElementName=parentTab, Path=DataContext.ClientPage"/>
</Setter.Value>
</Setter>
</DataTrigger>
<!-- Product -->
<DataTrigger Binding="{Binding}" Value="{x:Static vm:PageType.Product}">
<Setter Property="Header" Value="Product" />
<Setter Property="Content">
<Setter.Value>
<view:ProductView DataContext="{Binding ElementName=parentTab, Path=DataContext.ProductPage"/>
</Setter.Value>
</Setter>
</DataTrigger>
As you can see each DataTrigger is simply checking to see which enum its DataContext has been set to and setting it's own Header and Content accordingly.

Hiding TreeView items

I've been trying to just hide items from a TreeView. I'm using a custom data type as source (called SettingsMenuItem) which inherits from FrameworkElement (currently FrameworkContentElement, because otherwise the TreeView renders them wrong).
My goal is by setting the VisibilityProperty of these FrameworkElements to either Collapsed or Visible that I'm able to hide certain items (including their children). I know that this can be done by deleting items from the source collection. But that's not what I want. It would mean that I have to mirror each collection in order to keep track of it's actual items, bind to each one in order to be notified about Visibility-changes and create a new collection each time one changes. A lot of overhead for this.
Right now I have no clue how I could accomplish that. I figure it's related to the ItemsGenerator, but I haven't seen any possibility to override it's behaviour. I thought TreeView would be able to detect Visibility, but obviously it doesn't. As alternative I thought of a custom TreeViewItem (maybe even TreeView if necessary) - but at this point the abstraction of this whole system overwhelms me. I don't know where to start and what is actually necessary to solve the problem.
Tips what I have to change or implement by myself would be more than enough. A complete solution would be nice.
You can do this using a data trigger bound to a property (e.g. "IsVisible") in you tree data nodes:
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsVisible}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TreeView.ItemContainerStyle>
While this technically answers your question I would be wary of actually doing it. User3690202's comment is correct, it's the sort of thing you would normally do via filtering in your view model.
For alternate solution using code behind xaml.cs:
To Remove a specific TreeViewItem from a TreeView which is created from a code behind.
TreeViewItem treeViewItem1 = new TreeViewItem
{
Visibility = Visibility.Collapsed,
};
use the code with TreeViewItem you want to hide in a if condition to hide specific TreeViewItem Header let say "Cars" and you want to hide it and use the code with if condition to hide "Cars" TreeViewItem.

WPF - Why is my property not being updated?

Here's some code:
<ListBox
MaxWidth="468"
SelectionMode="Extended"
ItemsSource="{Binding Visitors, Mode=TwoWay}">
<ListBox.ItemContainerStyle>
<Style TargetType={x:Type ListBoxItem}>
<Setter Property="IsSelected" Value="{Binding VisitorSelected, Mode=TwoWay}" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
I need Visitors to be updated (at the source) whenever a visitor is selected. For some reason, What I have doesn't work. I'm racking my brain, but I can't think of a way around this. Ideas?
EDIT: Sorry, I accidentally had a duplicated property in there, which I removed. Also, let me clarify something: VisitorSelected IS getting updated. However it's not doing me much good, because I need to be notified in the view model containing Visitors - not where VisitorSelected is defined. I hope this makes sense
I ended up adding an Action parameter to the visitor item type, then calling that when VisitorSelected is set. The Action is passed in by the view model.

Since a selection list, how can I set the navigation sequentially?

I'm starting a new project in WPF and am now looking into using Prism. For now I'm simply trying to set up the navigation of the application using Prism. Unfortunately, my lack of experience with the framework makes it a bit difficult to get started.
To be more precise about my first challenge I have an application with a "navigation/menu" region and a "main" region.
In "navigation/menu" region, I have several checkboxes, in this case we have four of them, which represents a sequential navigation. I.E. we've selected View 2 and View 4.
So, when the user click Start, in "main" region must appear each view selected in that order. Check the below image, View 2 is first. Then when the user press next, must show View 4.
I mean on a more structural level..
if I could only get through the first steps..
Prism support TabControl Region Adapter, navigation can be done using standard requestNavigation method.
You need add all your tab content using Region.Add method to the region in your module's init phase.
view:
<TabControl prism:RegionManger.RegionName="tabRegion" />
C# code:
IRegionManager manager;
manager.Regions["tabRegion"].Views.Add(Container.Resolve(typeof(YourViewType)));
In your viewModel, you should write you navigation command:
public void NextView() {
regionManager.RequestNavigation("tabRegion", new Uri("YourViewType", UriKind.Relative));
}
bind to your "next" button:
<Button Command="{Binding NextViewCommand}" />
If you want to control whether user can navigate to next page, you can implement INavigationAware interface.
If you don't want lost data between navigation, you can make your view model has ContainerMangedLifeCycle or implement IsNavigationTarget method to return true.
Sorry for untested code sample, but you should get the point.
Create a class named ViewVM with a property IsSelected. Must implement INotifyPropertyChanged.
Add an ObservableCollection<View> named Views to your datacontext. Populate it with new instances of ViewVM.
Put an ItemsControl in your Window, with ItemsSource set to Views. The DataTemplate for the ItemsControl items should contain a CheckBox (with IsChecked bound to IsSelected) and a Label.
Add a TabControl to your Window, with ItemSource set to Views. Add a Style for TabItem such that TabItems are only visible if IsSelected is true.
Following the above steps will give you a window containing a list of views with checkboxes, as you requested, and a TabControl displaying only the selected views. Below is the XAML (I have tested this):
<StackPanel>
<ItemsControl ItemsSource="{Binding Path=Views}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<CheckBox IsChecked="{Binding Path=IsSelected}"></CheckBox>
<TextBlock Text="{Binding Path=Title}"></TextBlock>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TabControl ItemsSource="{Binding Path=Views}">
<TabControl.ItemTemplate>
<DataTemplate>
<Grid>
<TextBlock Text="{Binding Path=Title}"></TextBlock>
</Grid>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.Resources>
<Style TargetType="TabItem">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsSelected}" Value="False">
<Setter Property="Visibility" Value="Collapsed"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</TabControl.Resources>
</TabControl>
</StackPanel>
This addresses the structural/design aspect and should give you a good start to creating your solution - you'll also need to create a custom control to use instead of the TabControl. Instead of having tabs, your custom control should contain Next and Previous buttons to navigate between views.

How to change the way the selection is handled in a WPF TreeView

when I remove an item that is currently selected from the TreeView automatically the parent gets selected. I would like to change this behavior so the previous or the next child gets selected. I really don't know where to start ...
Any idea on how to accomplish this would be great!
Thanks.
You can set the SelectedItem by introducing a property such as IsSelected in your tree view node's datacontext class or model.
Assuming that you are binding a hierarchy of TreeViewItemModel class to the TreeView, you need to do the following
Add writeable IsSelected propertyb in TreeViewItemModel. Remember to raise property changed notification in the Setter of IsSelected.
Introduce this in the TreeView resources ...
<TreeView.Resources>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.Resources>
After you delete a node, set the next or previous tree view child's TreeViewItemModel objects IsSelected as true.
Let me know if this helps.
TreeView has Items property, which is of type ItemCollection. This type has some good events, like CollectionChanged or CurrentChanged. may be you should to spade this way?

Categories

Resources