Two-way binding with nested ViewModels - c#

Using Catel 3.9 and DevExpress 15.x
My Customer has asked that I make some UI changes to an app and I'm not sure it can easily be done.
Architecture:
There is a MainWindow with an associated View and ViewModel.
The MainWindow holds a TabControl where each Tab's content is a separate View/ViewModel. The MainWindow ViewModel does NOT own any of the nested VMs; they are constructed automatically by Catel at runtime by the View.
The old UI had buttons on each TabItem which allowed the Customer to load, save, display, filter, etc. The commands/properties for these buttons were bound directly to the ViewModel for that tab and was working fine.
The Customer would rather have a single top level (on the MainWindow) menu and selections from that menu would affect whichever tab had the current focus.
I can pass commands (using Messaging or Catel's InterestedIn attribute) to the correct ViewModel, but I'd like to have a more direct binding with the top level menu and the appropriate ViewModel so I can enable/disable menu items or even modify the text to suit whichever tab is open.
I'm looking for a primarily XAML and/or Catel solution. If you need additional information, please let me know.
Suggestions?
thanks,
randy
Edit: Sorry that I didn't include additional research. If you knew me, you'd know I will spend hours/days looking for solutions to problems and only when I'm stumped will I ask for assistance. My bad for not including more.
The hardest part about this issue is defining good search parameters. Most of the suggestions were similar to: Just put everything into the MainWindow ViewModel which (to me) is not a good design choice because what is displayed on the tabs IS different and should be separate.
Other solutions were to have the MainWindow ViewModel construct each of the inner ViewModels and then manage them. With the Catel framework I'm using, the framework automatically constructs the VM when the View is loaded, injecting any required parameters to the constructor. See below -- you just reference the View and Catel will match it up with its ViewModel and create it for you. Unfortunately without taking other steps, you really don't have a reference to the VM that was created.
MainWindow.xaml:
<dx:DXTabControl x:Name="MainTabControl"
Grid.Row="1"
Margin="10"
BorderThickness="0"
SelectedIndex="{Binding SelectedTabIndex}"
>
<dx:DXTabItem Header="Getting Started" IsEnabled="True">
<views:GetStarted />
</dx:DXTabItem>
<dx:DXTabItem Header="Validate Student Records" Background="Ivory">
<views:StudentValidation />
</dx:DXTabItem>
<dx:DXTabItem Header="Validate Teacher Records" Background="Ivory">
<views:TeacherValidation />
</dx:DXTabItem>
<dx:DXTabItem Header="Validate Admin Records" Background="Ivory">
<views:AdminRecordValidation />
</dx:DXTabItem>
</dx:DXTabControl>
Examples of some possible solutions I'm looking at:
One ViewModel Member of Another
Get a property of a tabitem's ViewModel when it's selected
Edit #2: There was a suggestion about using a Service and SO doesn't allow you to add detailed comments (and it puts a TIMER on your comments -- SHEEESH!), so I'll put my response here.
Consider this scenario using a Service: The Customer starts the app and clicks on a Tab (StudentValidation). The MainWindowViewModel (via a Property) detects the selected tab and calls the Service with an update (I'm not sure what is updated; possibly some sort of State). The "nested" ViewModels are notified (via an Event) of the change in the Service. I'll assume the StudentValidationViewModel is the only one who actually responds to the event and interacts with the Service, retrieving "data".
So, now we have the StudentValidation tab displayed and the Customer goes to the Main Menu of the app. The Main Menu is STILL tied to the MainWindow and every command is bound to the MainWindowViewModel. How does the Service bind the Main Menu to the ViewModel of the currently selected tab so that the commands will be handled by the StudentValidationViewModel? I'm probably missing something.

Use a singleton model which holds the shared data so you get the instance from wherever you like.

Services are the solution. Create a solution that is injected into all view models. Then the top-level vm can update the service, and all vm's can respond to the update via events.
Remember that vm's just represent a live view in memory so you can interact with them.

Thank you for all the suggestions. I tried to upvote each one, but as a "newbie" I can't. As I worked thru each one, I realized that all I wanted to do was bind a specific subset of the Main Menu items to the View/ViewModel of the currently focused tab on the Main Window. It seemed as simple as changing the DataContext of the menu item.
Here's the Main Menu. The FileSubMenu is the one I need to bind to the currently focused ViewModel. The other menu items can be handled by the MainWindowViewModel.
MainWindow.xaml:
<dxb:MainMenuControl Grid.Row="0"
HorizontalAlignment="Left"
HorizontalContentAlignment="Stretch"
VerticalAlignment="Top"
BarItemDisplayMode="ContentAndGlyph"
>
<dxb:BarStaticItem Content="Validator" Glyph="pack://application:,,,/LexValidator;component/Images/lex-logo.png" />
<dxb:BarSubItem x:Name="FileSubMenu" Content="File">
<dxb:BarButtonItem Content="{Binding LoadRecordsText}" Glyph="{dx:DXImage Image=LoadFrom_16x16.png}" Command="{Binding LoadRecordsFile}"/>
<dxb:BarButtonItem Content="Clear Display" Glyph="{dx:DXImageOffice2013 Image=Clear_16x16.png}" Command="{Binding ClearDisplay}"/>
<dxb:BarButtonItem Content="FTE Counts..." Glyph="{dx:DXImage Image=TextBox_16x16.png}" Command="{Binding ShowFTECounts}"/>
<dxb:BarButtonItem Content="Show Pay Grid..." Glyph="{dx:DXImage Image=Financial_16x16.png}" Command="{Binding ShowPayGrid}"/>
<dxb:BarItemLinkSeparator />
<dxb:BarCheckItem Content="Show Ignored Issues" Glyph="{dx:DXImage Image=ClearFilter_16x16.png}" IsChecked="{Binding ShowIgnoredIssues}" IsEnabled="{Binding IsShowIgnoredIssuesEnabled}" />
</dxb:BarSubItem>
<dxb:BarSubItem Content="Exit">
<dxb:BarButtonItem Content="Exit" Glyph="{dx:DXImage Image=Close_16x16.png}" Command="{Binding ExitApplication}"/>
</dxb:BarSubItem>
<dxb:BarSubItem Content="Help">
<dxb:BarButtonItem Content="About..." Glyph="{dx:DXImageGrayscale Image=Index_16x16.png}" Command="{Binding ShowAboutBox}"/>
</dxb:BarSubItem>
</dxb:MainMenuControl>
Then on the TabControl, I handle the event when a new tab is selected:
MainWindow.xaml.cs
private void MainTabControl_SelectionChanged(object sender, TabControlSelectionChangedEventArgs e)
{
// Turn it off and see if it needs to be enabled.
this.FileSubMenu.IsEnabled = false;
var newTabItem = e.NewSelectedItem as DXTabItem;
if (newTabItem != null)
{
var tabView = newTabItem.Content as UserControl;
if (tabView != null)
{
var tabViewModel = tabView.ViewModel;
if (tabViewModel != null)
{
this.FileSubMenu.DataContext = tabViewModel;
this.FileSubMenu.IsEnabled = true;
}
}
}
}
I realize this may not be very "MVVM", but it works well and the "Boss" said "Move on to something else". I would be happier if the above code could be handled totally in XAML -- some sort of resource maybe?
Again, if there is something I missed or a better (more MVVM) solution, please let me know.

Related

Dynamic Detail View C# Xaml

I want to program a dynamic Detail View. Like a user clicked on an Item then he sees the detail view. Now he sees all Values, but when he only want to see a few values, he click on a config Button in this view and a second view opens where he can select and unselect all types of values. Like he dont want to see the Description, the he deselect it in the second view, and it´s no longer visible in the first view.
The only way for me to implement something like this is to programm a Function which generates the first view. The view would be a UI-Element. Which is then returned to the Windows where the UI-Element is set a child of an Element on the Window. But I think this isn´t a good way. How do you would solve this problem?
Thanks for every hint :)
If I understand well you want
List -> Details -> MoreDetails/Edit
Depending on what platform you are creating is a bit different but the idea is the following:
<ItemsControl ... x:Name="ItemsList" ItemsSource="{Binding}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<Interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="Tapped">
<core:InvokeCommandAction
Command="{Binding Datacontext.ShowItemDetails, ElementName=ItemsList}" CommandParameter="{Binding}"/>
</core:EventTriggerBehavior>
</Interactivity:Interaction.Behaviors>
</Grid>
</DataTemplate>
</ItemsControl>
Now when you tap the grid you can show a PopupControl with the details and the DataContext:
public Command<ItemClass> ShowPopup
{
get
{
return new Command<ItemClass>((i)=>
{
//Create the Popup
});
}
}
In case you are not using MVVM you can add the Command in code behind and place in the page contructor this.DataContext = this; and place the previous command there.
And now create a control for the content of the popup, bind the properties to the item details, now add another behavior with a command in that control and unhide the controls for edit mode or more details mode
You could have for each detail item a property bool ItemXIsVisible which is bound to a checkbox in the config view, and to the IsVisible property of the X control in the detail view?

WPF: Which solution? TabControl with close button and new tab button

I'm trying to find the best solution for a TabControl that both support a close button on each TabItem, and always show a "new tab button" as the last tab.
I've found some half working solutions, but i think that was for MVVM, that I'm not using. Enough to try to understand WPF =)
This is the best solution I've found so far:
http://www.codeproject.com/Articles/493538/Add-Remove-Tabs-Dynamically-in-WPF
A solution that i actually understand. But the problem is that it is using the ItemsSource, and i don't want that. I want to bind the ItemsSource to my own collection without having to have special things in that collection to handle the new tab button.
I've been search for days now but cant find a good solution.
And I'm really new to WPF, otherwise i could probably have adapted the half done solutions I've found, or make them complete. But unfortunately that is way out of my league for now.
Any help appreciated.
I have an open source library which supports MVVM and allows extra content, such as a button to be added into the tab strip. It is sports Chrome style tabs which can tear off.
http://dragablz.net
This is bit of a dirty way to achieve the Add (+) button placed next to the last TabItem without much work. You already know how to place a Delete button next to the TabItem caption so I've not included that logic here.
Basically the logic in this solution is
To bind ItemsSource property to your own collection as well as
the Add TabItem using a CompositeCollection.
Disable selection of
the Add(+) TabItem and instead perform an action to load a new tab when it
is clicked/selected.
XAML bit
<TextBlock x:Name="HiddenItemWithDataContext" Visibility="Collapsed" />
<TabControl x:Name="Tab1" SelectionChanged="Tab1_SelectionChanged" >
<TabControl.ItemsSource>
<CompositeCollection>
<CollectionContainer Collection="{Binding DataContext.MyList, Source={x:Reference HiddenItemWithDataContext}}" />
<TabItem Height="0" Width="0" />
<TabItem Header="+" x:Name="AddTabButton"/>
</CompositeCollection>
</TabControl.ItemsSource>
</TabControl>
The code behind
private void Tab1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Contains(AddTabButton))
{
//Logic for adding a new item to the bound collection goes here.
string newItem = "Item " + (MyList.Count + 1);
MyList.Add(newItem);
e.Handled = true;
Dispatcher.BeginInvoke(new Action(() => Tab1.SelectedItem = newItem));
}
}
You could make a converter which appends the Add tab. This way the collection of tabs in you viewmodel will only contain the real tabs.
The problem is then how to know when the Add tab is selected. You could make a TabItem behavior which executes a command when the tab is selected. Incidentally I recommended this for another question just recently, so you can take the code from there: TabItem selected behavior
While I don't actually have the coded solution, I can give some insight on what is most likely the appropriate way to handle this in a WPF/MVVM pattern.
Firstly, if we break down the request it is as follows:
You have a sequence of elements that you want to display.
You want the user to be able to remove an individual element from the sequence.
You want the user to be able to add a new element to the sequence.
Additionally, since you are attempting to use a TabControl, you are also looking to get the behavior that a Selector control provides (element selection), as well as an area to display the element (content) which is selected.
So, if we stick to these behaviors you'll be fine, since the user interface controls can be customized in terms of look and feel.
Of course, the best control for this is the TabControl, which are you already trying to use. If we use this control, it satisfies the first item.
<TabControl ItemsSource="{Binding Path=Customers}" />
Afterwards, you can customize each element, in your case you want to add a Button to each element which will execute a command to remove that element from the sequence. This will satisfy the second item.
<TabControl ...>
<TabControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Path=CustomerId}" />
<Button Command="{Binding Path=RemoveItemCommand, Mode=OneTime,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type TabControl}}"
CommandParameter="{Binding}" />
</StackPanel>
</DataTemplate>
<TabControl.ItemTemplate>
</TabControl>
The last part is a bit more difficult, and will require you to actually have to create a custom control that inherits from the TabControl class, add an ICommand DependencyProperty, and customize the control template so that it not only displays the TabPanel, but right next to it also displays a Button which handles the DependencyProperty you just created (the look and feel of the button will have to be customized as well). Doing all of this will allow you to display your own version of a TabControl which has a faux TabItem, which of course is your "Add" button. This is far far far easier said than done, and I wish you luck. Just remember that the TabPanel wraps onto multiple rows and can go both horizontally or vertically. Basically, this last part is not easy at all.

WPF MvvM communication between view models in a single View

This title might be inappropriate for question itself but stay with me  I’ll change it if you have better suggestion. This is my first wpf application so I might missed some key concept… I did google, but I failed to find correct approach.
I am building wpf application using MvvM Light and MUI and I got into trouble with item bindings, ie communication between view models. Now, I am sure that I wouldn't have this problem if I used single View model for the Page and all user controls in it, but I think I overdid it on my first try.
I have one Main Window in application and pages as the user controls. In each page I have several other user controls and each one of them have its own view model and its own logic for doing stuff but in the end they all depend on the corresponding VM with data grid. We could think of them as poor man angular directives. Each user control have its data context defined like so:
DataContext="{Binding ViewModelName, Source={StaticResource Locator}}
Layout looks like this: Wpf Layout
Look at this way, DG1 in VM1 is Master (customer) and UC3 and UC4 are Details (orders). If I add new order to the customer, I would like it to be updated in the DG1 without refreshing entire grid.
In VM1 Data Grid 1 selection changed I am firing commands to set property values of depending user controls.
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<mvvm:EventToCommand Command="{Binding ErrorWorkflow.GetErrorWorkflowCommand, Mode=OneWay, Source={StaticResource Locator}}" CommandParameter="{Binding SelectedError.WF_REF}" />
<mvvm:EventToCommand Command="{Binding ErrorDetails.GetErrorCaseDetailsCommand, Mode=OneWay, Source={StaticResource Locator}}" CommandParameter="{Binding SelectedError}" />
</i:EventTrigger>
</i:Interaction.Triggers>
That part works ok, but when I change the value of in the depending VM3 for both VM3 and VM1, VM1 property values are not changed even though I call RaisePropertyChanged or setting property explicitly by hand like SelectedError.Status = “somethingnew”.
On the other hand, if I clear selection form the data grid, depending view models stay bound (text boxes on them preserve the value because they are referencing properties on their own VM3).
All View Models derive from ViewModelBase from mvvmLight and all models from ObservableObjects (I know that I should use Poco, but apparently I have to create each property on VM too). Example:
public const string SelectedErrorPropertyName = "SelectedError";
private ErrorLog _selectedError;
public ErrorLog SelectedError
{
get
{
return _selectedError;
}
set
{
Set(() => SelectedError, ref _selectedError, value);
}
}
I think that Messenger would be an overkill considering the size of the application (only few pages like this one).
Should I change the Page to use only one View Model and share them for each user control or am I missing something obvious here?
If you think that I am missing some key information in this example please tell me and I’ll update.
Thank you in advance for any advice, cheers!
You should never change the Page to only one View Model and Messenger is not an overkill. The MVVM Light Messenger is built to solve exact the problem (communication between VMs) you are having at the moment. You should use it.
For further information about the messaging within MVVM Light, Jesse Liberty of Microsoft has a great tutorial on how to make use of it.

ContextMenu won't go away after selection.

I have the following code that adds a context menu to a textbox on the UI. The user is supposed to be able to bring up the context menu and select a new units to be used. So the method CurrentUnits in my view model is bound to the textbox. I want a context menu populated by all the potential units. So the method Units in my view model returns a string[] of unit options; such as inches, cm, feet, meters, etc. When the user selects one the method NewUnits_Click is invoked. All works fine, however the contextmenu does not go away when the user selects a menu option. Pressing somewhere else on the screen like the application menu bar will then clear it. Has anyone else seen this problem, or see something wrong with the code below. It seems to have something to do with the ItemTemplate/DataTemplate I have, as creating an set of menu items by hand works fine.
<TextBlock Width="100" Text="{Binding CurrentUnits}" TextAlignment="Right">
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu ItemsSource="{Binding Units}">
<toolkit:ContextMenu.ItemTemplate>
<DataTemplate>
<toolkit:MenuItem Header="{Binding}" Click="NewUnits_Click" />
</DataTemplate>
</toolkit:ContextMenu.ItemTemplate>
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
</TextBlock>
If you aren't using MVVM when working with WPF, I highly suggest you to start doing so. And even if you are not, I suggest that instead of binding to Click you instead bind on the Command property which wants something that implements ICommand. I think that the behavior you are getting is intended, buttons and menu items in WPF are intended to bind to Commands, it's not just WinForms 2.0.
Another solution would be to hide the Context menu in the code-behind. Perhaps this resource will help you in achieving that.

How to handle WPF event in MVVM for nested controls in a Window

FINAL NOTE
Final solution found in another post
Although I appreciated the clarification that was provided, the ultimate solution was in-fact provided by another solution as linked above. No matter WHAT I tried, the binding via the "Element Name" component was not working. I had to go based on the "Relative" hierarchy up the datagrid...
<Button Name="btnPrintReport"
Command="{Binding DataContext.MyPrintCommand,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type DataGrid}}}"
CommandParameter="{Binding}"
Height="16" Width="16" HorizontalAlignment="Center" >
<Image Source="MyButtonImage.png" IsHitTestVisible="True"/>
</Button>
Hope something not too complicated in WPF / MVVM environment. Here's the scenario.
I have a Window (.xaml) and a corresponding View Model (.cs). The form displays fine with all the data bindings no problem. (note: this is NOT done via any commercial "framework" )
One of the controls that is in the view window is a custom user control of a datagrid with all pre-defined columns, headings and content to be displayed when the view is shown. This works all no problem even though the control is not directly "defined" in the main window .xaml file, but just dropped on the form as the user control itself (which has its own obvious .cs code-behind).
With the main window's "DataContext" pointing to the View Model, and the user control that has a datagrid
<DataGrid AutoGenerateColumns="False"
Name="dataMyStuff"
ItemsSource="{Binding Path=MyTablePropertyOnViewModel,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}" ... />
Now, what I'm looking for. On this data grid, I have a column that has an image in the first column. When I click on this image, I want to print a report specific to the record as represented by this row (it has a PK value I use). So, how do I tell the image "KeyUp" event to go to the View Model event handler since that is where the data is, and some other methods I'll need for preparing the call to the report. The view portion of the grid is for cosmetic display to the user, and thus no "functionality" directly in this control.
-- EDIT -- per progress from answers
I've adjusted my datagrid per comments from Josh and Rachel, however, something still does not appear to be quite right... Seeing the button was using a "Command" instance, I interpreted this as it needed to attach to an instance of an "ICommand" interface object on my view model. So, I created an instance. I know the command handler works as it is also used for common things like Add, Edit, Save, Cancel, Exit, etc... So I have a new one for this printing purpose. For simplicity, I have it created as an Always Execute, so there is no method to handle the "CanExecute" portion of the control. I've set the button's "Command" to almost all iterations I could think of an still nothing, but here's an update of what I'm seeing going on.
<UserControl>
<Data grid columns / template, etc to the button>
<DataTemplate>
<Button Name="btnPrintReport"
Command="{Binding DataContext.MyPrintCommand}" >
<Image Source="myPrintImage.png"/>
</Button>
</DataTemplate>
</Data grid columns, etc>
</UserControl>
In my ViewModel class (myICommandButtonHandler inherits from ICommand)
private myICommandButtonHandler myPrintCommand;
public myICommandButtonHandler MyPrintCommand
{
get { if (myPrintCommand == null)
myPrintCommand = new myICommandButtonHandler(myPrint);
return myPrintCommand;
}
}
private void myPrint()
{
MessageBox.Show( "Doing the print job..." );
}
Now, what I'm seeing. During step through initialization of all the controls and such. I click menu item to call my Window to be displayed. FIRST, it creates an instance of the View Model controller. THEN, it calls the Window and passes in the View Model controller as parameter so it is immediately set at the Window level as the "DataContext" of the window. The main window then goes into it's "InitializeComponents" call and starts to build out all the other controls embedded, including this separate class that contains the data grid in question. At the constructor of this usercontrol (that has the datagrid), there is no "data context" set yet as the rest of the controls have not been initialized yet, and I don't know why / when the "bindings" apparently get "hooked" in. So, it appears that trying to do the binding to the data grid's command button are failing. HOWEVER, at run-time, the actual data IS updated in the grid, so I know that much is working.
So, the data grid has its "ItemsSource" set to a property of a "DataView" property on the view model, but the binding of the "button" doesn't appear to see the "MyPrintCommand" handler that I thought would get the hit.. and its action is to just display a message box (for now).
Usually I use an AttachedCommand Behavior which allows me to bind Events to ViewModel Commands. For example, you could use
<Image ...
local:CommandBehavior.Event="KeyUp"
local:CommandBehavior.Command="{Binding DataContext.PrintCommand, ElementName=dataMyStuff}"
local:CommandBehavior.CommandParameter="{Binding }"/>
I'd would recommend using a different event than KeyUp, since I don't think Images can have Keyboard focus so the KeyUp event will never get fired.
A better alternative is to use a Button and overwrite it's Template to be your Image. This will maintain the Click functionality, and give you access to Command and CommandParameter properties
<Button Command="{Binding DataContext.PrintCommand, ElementName=dataMyStuff}"
CommandParameter="{Binding }">
<Button.Template>
<Image ... />
</Button.Template>
</Button>
Also, the CommandParameter="{Binding }" will simply pass the current DataRow's DataContext (your data object) to the command
Change the data template to be a button that has a image as its content. Use the command and commandparameter properties on the button to call your printing method. You can declare your print command in your viewmodel, and bind to it. Your parameter could be the selected row in your datagrid.

Categories

Resources