WPF MVVM Light - About views communicating - wpf

I am currently working on a project for work. I am seeking an outside design opinion, as well as some general information on the issue I am faced with.
We have a MainWindow.xaml file that is located in the root directory of the project. In this main window is some design and logic for some collapsing stack panels, ribbon toolbar, etc.
So far the idea is to include a different in each stack panel to help make the code neat. The views are located in a 'Views' folder. So just to be clear, the MainWindow.xaml and other views ARE NOT in the same directory. This is open to change, if necessary.
So here is my question/issue: We have a Window ('A'), a main panel with a collapsable stack panel with some information ('B') that is contained in window 'A'. Then there is another stack panel to manage the contents in 'B', (collapse/visisble) ('C').
'A' contains a toggle button to show/collapse 'B'.
'B' contains a button to show/collapse 'C'.
'C' contains a button to show/collapse itself, 'C'.
'C' should have its logic all contain within a view, so the MainWindow ('A') should have a simple tag:
<StackPanel Style="{StaticResource FrameGradient}" Tag="{Binding ElementName=ToggleButton}">
<view:Content></view:Content>
</StackPanel>
Currently, the bindings for toggling the buttons within 'A' are in the styling. The In this case FrameGradient has triggers like so:
<Style x:Key="FrameGradient" TargetType="{x:Type StackPanel}">
//Setter properties
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="StackPanel.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
Is it possible to, within the 'Content' View to TOGGLE the panel, 'C', which is NOT within the view? I feel like I am missing a core idea of XAML here. I found a 'cheap' work around which is to place the 'Close' button from the Content View outside of the tags, but then that leads to styling issues and I feel like I shouldn't have to do something silly like that. Again, the idea is that the toggle button for Stack Panel 'C' is contained within another view and I want to be able to toggle it from another view.
I apologize if I am not clear enough, I will provide whoever asks with more information if required here.
UPDATE
I have some time to actually add the code I am using so that this might make more sense.
MainWindow.xaml - Logic for Filter panel (Located in root)
<StackPanel Grid.Row="1" Grid.Column="4" Visibility="Collapsed" Style="{StaticResource FrameGradient}">
<Grid x:Name="FilterContentGrid">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<view:Filters></view:Filters>
</Grid>
</StackPanel>
Filters.xaml - Logic for Filters view (Located in /Views)
The button within the file that needs to Collapse the above StackPanel.
<Button x:Name="FilterManagementCloseButton" Content="CLOSE"></Button>
Theme.Xaml - Logic for all styling (Located in root, along with MainWindow.xaml and App.xaml)
Button Styling
<Style x:Key="FilterManagementCloseButton" TargetType="Button">
<Setter Property="Padding" Value="10,5,20,3" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource AncestorType={x:Type Local:MainWindow}}}" Value="True">
<Setter Property="StackPanel.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
And finally, the FrameGradient Styling also located in Theme.xaml
<Style x:Key="FrameGradient" TargetType="{x:Type StackPanel}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Tag.IsChecked, RelativeSource={RelativeSource Self}}" Value="True">
<Setter Property="StackPanel.Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
SO, I hope this makes things more clear. I want the CLOSE button within Filters.xaml to COLLAPSE the stackpanel that is located in MainWindow. I realize this code is a mess at the moment.

Is it possible to, within the 'Content' View to TOGGLE the panel, 'C',
which is NOT within the view?
Create a shared VM which each other VM will have a property for which it can access. This VM can be loaded with during initialization of the other VMs. To allow for changes to happen put INotifyProperty(ies) on the shared VM which will then flag the desired logic across all views. Finally bind the target control(s) to your datacontext as normal except sub path into the shared VM target's property.
Hence when one view toggles (two way binding) a shared property it is reflected on the view of the target panel.
Update Example
The idea here is that one creates a viewmodel for the AppPage. That VM will hold generic flags which are shared across all viewmodels. Each subsequently created ViewModel will have a reference to the AppPage's viewmodel.
The example below is a mainpage where the AppVM contains a flag which informs the mainpage whether a login is in process. If it is and that value is true then a bound button on the mainpage will be enabled.
Subsequently the mainpage can override the appvm and put a new value within that flag by a bounded checkbox that can in-directly change whether the button is enabled; thus changing the flag for all other VMs in the process.
Here is the Mainpage VM, for this example I simply create the AppVM, but it could be passed in, or gotten from a static reference elsewhere. NOTE also how I don't care when AVs (appVM) property changes; it is not required for this example (we are not binding anything to AppVM, just its properties which need to be monitored).
public class MainVM : INotifyPropertyChanged
{
public AppVM AV { get; set; }
public MainVM()
{
AV = new AppVM() { LoginInProcess = true };
}
}
Here is the AppVm
public class AppVM : INotifyPropertyChanged
{
private bool _LoginInProcess;
public bool LoginInProcess
{
get { return _LoginInProcess; }
set { _LoginInProcess = value; OnPropertyChanged(); }
}
}
Here is MainPage's Xaml where the datacontext has been set to an instance of MainVM:
<StackPanel Orientation="Vertical">
<CheckBox Content="Override"
IsChecked="{Binding AV.LoginInProcess, Mode=TwoWay}"/>
<Button Content="Login"
IsEnabled="{Binding AV.LoginInProcess}"
Width="75" />
</StackPanel>
I base the MVVM off of my blog article Xaml: ViewModel Main Page Instantiation and Loading Strategy for Easier Binding which explains the other missing items of this example such as the mainpage's datacontext loading.

You can use RelativeSource Bindings to bind from child views to properties in parent view models. Let's say you have a ToggleButton in MainWindow.xaml that is data bound to a property named IsChecked, which is declared in the object that is data bound to the MainWindow DataContext property. You could data bind to that same property from any child view with a RelativeSource Binding, like this:
<Style x:Key="FrameGradient" TargetType="{x:Type StackPanel}">
<Setter Property="StackPanel.Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=DataContext.IsChecked, RelativeSource={
RelativeSource AncestorType={x:Type Local:MainWindow}}}" Value="False">
<Setter Property="StackPanel.Visibility" Value="Collapsed" />
</DataTrigger>
<!-- Note that there is no need for two Triggers here -->
<!-- One Setter and one Trigger is enough -->
</Style.Triggers>
</Style>

Related

WPF trigger can't change a property in mvvm

I wanted to make it happened to change property in ViewModel using Trigger tag in xaml.
The code I made was like this.
<Grid x:Name="LogoGrid">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Height" Value="{Binding LogoHeight, Mode=TwoWay}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType=Grid}, Path=ActualHeight, Converter={StaticResource ImageHeight}}" Value="False">
<Setter Property="Height" Value="150"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<Image Source="/Resources/Logo/Logo.png" Style="{StaticResource GameWindowLogoStyle}"/>
</Grid>
I bound data to LogoHeight property in ViewModel and I wanted to set this property to 150 when parents grid is big enough to show Logo Image.
It works fine in the UI. but it didn't change LogoHeight property.
It is the same things keep happening when I use animation with Storyboard tag.
Is it normal things that propery can't be changed by Trigger tag or animation in the Storyboard tag.
Please tell me the way if there is a way to change property in Trigger tag or Storyboard tag.
Thank you.
A Setter in a Style cannot set the property of a view model. It can only set a property of the element to which the Style is being applied, i.e. the Grid in this case.
You may be able to work around this by using a Storyboard that animates the source property:
Setting a ViewModel boolean property inside a storyboard

DataTrigger Binding to a ToggleButton within another namespace

I try to access a ToggleButton that is within a separate UserControl to Trigger a DockPanel.Style DataTrigger.
Here is how I made it work when both, the ToggleButton and the DockPanel, are in the same namespace:
<ToggleButton x:Name="OneToggleButton"
Content="Click me..." />
<DockPanel>
<DockPanel.Style>
<Style>
<Setter Property="UIElement.Visibility"
Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked,
ElementName=DetailsBookToggleButton}"
Value="False">
<Setter Property="UIElement.Visibility"
Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DockPanel.Style>
<TextBlock DockPanel.Dock="Top" Text="..." />
</DockPanel>
But now when I move the ToggleButton into an other file (other namespace) it doesn't work anymore. ElementName (as I understand it) only works for elements within the same file.
So how can I manage a Binding to the IsChecked of my ToggleButton in another file?
Anybody have a suggestion? Would be great :)
FYI, the term you are looking for is "name scope", and there is no way to reference an element defined in another name scope. Arguably, you should not be allowed to do this.
Rather than binding one UI element to another, consider binding them both to a common property, either in your view model, on some common ancestor element, or via dependency property inheritance.

Can't change content-template dynamically

I have a ContentControl that I need to set it's ContentTemplate dynamically.
so I decided to write 2 DataTemplates, and then style my ContentControl such that a trigger fires and set the proper template (dt1/dt2) when a Boolean dependency property in my view-model changes (true/false).
But the problem is if the Boolean property is primarily set to true, the data template will always be dt1 and changing the property to false wont change the template to dt2.
since the data triggers are bound to the Boolean dependency property, shouldn't changing the property result in firing the triggers?
notes:
There is a button in MyView which changes BooleanDependencyProp on it's
click event.
MyViewModel inherits from an interface that
implements INotifyPropertyChanged.
Xaml:
<UserControl x:Class="Views.MyView">
...
<StackPanel>
<ContentControl Content="{Binding RelativeSource={RelativeSource AncestorType=MyView}, Path=MyViewModel}">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding BooleanDependencyProp}" Value="true">
<Setter Property="ContentTemplate">
<Setter.Value>
<dt1 ... />
</Setter.Value>
</Setter>
<DataTrigger Binding="{Binding BooleanDependencyProp}" Value="false">
<Setter Property="ContentTemplate">
<Setter.Value>
<dt2 ... />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</StackPanel>
This is a known shortcoming of the WPF framework, if you want to apply different data templates, consider either using the visual state manager to change the presentation, or swap out the content data template for a user control that changes based on the triggers instead, you'll get more mileage.
There's a lot more I could say, but it would involve knowing your scenario and the differences in these DataTemplates, why you are disambiguating, etc. Also, MVVM all around? or straight ahead Code+Markup style with a few view models?

Switching between views according to state

Say I've an application that displays user friends list. The friends list is displayed in as a TabItem. The user has to Log in first to the server, in order to get the list of friends.
I've created two user controls, one for when the user is logged in, the the other when he is unlogged. something alone this line:
UnloggedView.xaml
<UserControl x:Class="UnloggedView" ...>
<TextBlock ...>You need to <Hyperlink Command="{Binding LoginCmd}">
Login</Hyperlink>too see your friends list</TextBlock>
</UserControl>
LoggedView.xaml:
<UserControl x:Class="LoggedView" ...>
...
<ListView ItemSource={Binding Path=friends}">...
</UserControl>
The main window has the following code:
....
<TabItem Header="Friends">
<vw:UnloggedView />
</TabItem>
I believe everything is according to the MVVM principal. The LoginCmd is a simplified variation of the DelegateCommand (from prism), implemented in the ViewModel. Both Views works fine, and as the list is populated (asynchronously), notification are fired and the View is updated. I'm happy.
So I've two questions: First question is how to I fire the LoginWindow (where the user is prompted to enter his credentials? For now, I simply create the LoginWindow (a view object) and presents it with ShowDialog. It appears like I'm breaking the rules of MVVM here by directly manipulating the UI from the ViewModel.
main question is after I log-in with the server, what is the correct way to replace the content of the TabItem with the LoggedView. According to the MVVM principals, the ViewModel shouldn't not have knowledge to the internals of the View. I expose IsLogged property in the ViewModel (which will fire PropertyChanged notification) but what should I bind to what in order to make everything happens? I really don't want the ViewModel manipulating the View.
Thanks
I see this question come up a lot, and have written something about switching between Views/UserControls here. Usually I use a ContentControl and switch out the ContentTemplate based on a DataTrigger, however the same principal works to switch a TabControl's ItemTemplate
<DataTemplate x:Key="LoggedOutTemplate">
<local:UnloggedView />
</DataTemplate>
<DataTemplate x:Key="LoggedInTemplate">
<local:LoggedView />
</DataTemplate>
<TabControl>
<TabControl.Style>
<Style TargetType="{x:Type TabControl}">
<Setter Property="ItemTemplate" Value="{StaticResource LoggedOutTemplate}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsLoggedIn}" Value="True">
<Setter Property="ItemTemplate" Value="{StaticResource LoggedInTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TabControl.Style>
</TabControl>
You may have to use ElementName or RelativeSource in your DataTrigger binding to find the IsLoggedIn property in your DataContext
As for firing a Login Command from your Logged Out view, there are multiple ways of doing it.
My preferred method is to use some kind of messaging system such as MVVM Light's Messenger, or Microsoft Prism's EventAggregator, and firing some kind of ShowLoginDialog message when the button is clicked, then let whatever ViewModel is taking care of showing the login dialog subscribe to receive those messages and handle them.
Another way is simply use a RelativeSource binding to find the object in the Visual Tree that has the LoginCommand in it's DataContext, and bind to that.
You can see examples of both here
First I'll answer you second question... Just create an Enum as SessonState
enum SesionState
{
LoggedOut=0,
LoggedIn
}
After that create a property in your ViewModel for window called SessionState and Update that Property with the required value when you Login and LogOut.
Xaml required for switching views
<Window>
<Window.Resources>
<DataTemplate x:Key="LoggedOutView">
<ViewLayer: LoggedOutView/>
</DataTemplate>
<DataTemplate x:Key="LoggedInView">
<ViewLayer:LoggedInView/>
</DataTemplate>
<Style x:Key="mainContentControlStyle"
TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=SessionState}"
Value="0">
<Setter Property="ContentTemplate" Value="{StaticResource ResourceKey=LoggedOutView}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Mode}"
Value="1">
<Setter Property="ContentTemplate" Value="{StaticResource ResourceKey=LoggedInView}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid>
<TabControl>
<TabItem>
<ContentControl Grid.Row="0"
Content="{Binding}"
Style="{StaticResource ResourceKey=mainContentControlStyle}">
</TabItem>
</TabControl>
</Grid>
</Window>
for your 1st question: you can simply use a ILogindialogservice from your viewmodel. i use the following for dialogs and mvvm. its "Unit Test"able and not breaking mvvm.
EDIT. in your viewmodel you would have then a line something like this:
var result = this.loginservice.ShowDialog("Login", loginvm);

FrameworkElement`s DataContext Property does NOT inherit down the element tree

Hello WPF Pros at least I hope some of you read this!
DataContext is a property on FrameworkElement (base class for all WPF Controls) and is implemented as a DependencyProperty. That means all the descendant elements in the logical tree share the same DataContext.
So the ContentControl should do it with its descendant elements right?
I have a scenario where that is NOT the case and I would like to know WHAT is the cause of that misbehaviour ?!
That you understand a bit more about it please read this thread ( dont NOT want to copy everything here) where the trouble starts...:
WPF: Can not find the Trigger target 'cc'. The target must appear before any Setters, Triggers
and to say it in short words: My DataTemplates within the ContentControl do have a dead DataContext that means there is NOTHING to bind to it, what is actually not possible...
Every Element down the ContentControl has NOTHING set in the DataContext Property ???
DataContext is a property on
FrameworkElement (base class for all
WPF Controls) and is implemented as a
DependencyProperty. That means all the
descendant elements in the logical
tree share the same DataContext.
The fact that it's a dependency property doesn't imply inheritance... It's true for DataContext, but only because the dependency property has the FrameworkPropertyMetadataOptions.Inherits flag in its metadata.
So the ContentControl should do it
with its descendant elements right?
ContentControl is a bit special: the DataContext of its descendants (the visual tree built from the DataTemplate) is actually be the Content of the ContentControl. So if your ContentControl has no content, the DataContext inside it is null.
This worked for me:
<ContentControl ContentTemplate="{StaticResource NotesTemplate}"
Content="{Binding}"
DataContext="{Binding HeightField}"/>
Without the Content="{Binding}", the DataContext was NULL
The last answer (from VinceF) worked for me too.
I wanted to show a usercontrol depending on the value of a property in my viewmodel. So I made a ContentControl with some Style Triggers. Depending on the value of a bind property the trigger sets a specific ContentTemplate containing the specific usercontrol.
The usercontrol was shown right, but its DataContext was always null. So I had to set the Context of the ContentControl to: Content="{Binding}" After that, the UserControls worked fine and had the same DataContext as their parent.
So my XAML looks like that:
In the Resources part I defined two DataTemplates; each one for each UserControl I want to show.
<DataTemplate x:Key="ViewA">
<namespace:UserControlA/>
</DataTemplate>
<DataTemplate x:Key="ViewB">
<namespace:UserControlB/>
</DataTemplate>
The part where I show the UserControl depending on a property is the following:
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Property}" Value="0">
<Setter Property="ContentControl.ContentTemplate" Value="{StaticResource ViewA}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Property}" Value="1">
<Setter Property="ContentControl.ContentTemplate" Value="{StaticResource ViewB}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
after reading this question and previous answers, I prefer using ContentControl with data triggered Content like this:
Controls which will be set as Content of ContentControl:
<TextBox x:Key="ViewA">
...
</TextBox>
<ComboBox x:Key="ViewB">
...
</ComboBox>
ContentControl which switch own content by DataTrigger in ContentControl style:
<ContentControl>
<ContentControl.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=Property}" Value="0">
<Setter Property="Content" Value="{StaticResource ViewA}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Property}" Value="1">
<Setter Property="Content" Value="{StaticResource ViewB}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
I hope this helps to someone like previous answers to me.

Resources