I am trying to learn MVVM and using MVVM light with my phone application but I am kinda confused on how to access some information.
I am trying to not to use code behind events as much as possible as that does not seem to be the true MVVM way but I ran into a problem I don't know how to do.
I am using Google authentication and I am checking the Naviagted Event after each browser load.
public ICommand BrowserNavigated
{
get
{
return new RelayCommand<NavigationEventArgs>(e =>
{
var d = e;
var a = d;
});
}
}
However I also need the actual object control(I want to access the html that page is spitting back out) but I don't know how to get it.
private void wbGoogle_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
var d = e;
var d2 = d;
}
in the above code I could just cast "sender" to a web browser object but with me doing it the MVVM way I don't know how to access it.
Should I have another property or something for WebBrowser in my ViewModel?
In MVVM, code behind is allowed, but perhaps bindings are preferred. However, having GUI controls / events (hard coupling) is not allowed.
There may be ways to avoid code behind, but if you have to handle an event, get the data out of the event and set the property on your ViewModel in your code behind, then that is a better way to do it than adding UI code to your ViewModel which is clearly not keeping with MVVM.
Perhaps you can create some sort of EventTrigger which sets a property for a webbrowser that you can databind to create a re-usable Trigger that you can set in your XAML? (There's probably lots of ways to be clever on how to avoid code behind and create reusable code)
Your ViewModel should be totally unaware of the View or particular controls. Whether or not to keep the codebehind of your view clear or not, is a matter of religion.
If you want to keep it clean, which I recommend whenever possible, there are a number of concepts, which allow you to do so.
First, you need to design your View/ViewModel relationship in a way, that all data relevant for the ViewModel is present 'at all times' in the ViewModel or can be passed to the ViewModel via CommandParameter of a ICommand. In your case, if the page of the Webbrowser is controlled by (i.e. might be set from) the ViewModel, the ViewModel should hold a property, which is bound to the Source property of the browser. If the ViewModel just needs to 'know' the Uri when the BrowserNavigated is executed, just pass it as a CommandParameter.
Secondly, for your particular case, you want to execute a command on the ViewModel, when the Navigated event of the WebBrowser is raised. As always, there are several options. I prefer the option which comes with the framework: The EventTrigger in System.Windows.Interactivity allows you to relay any event of any control to commands via bindings.
This way, the Uri can be set from the ViewModel:
<WebBrowser Source="{Binding BrowserPageUri}" Name="wbGoogle">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Navigated" >
<i:InvokeCommandAction Command="{Binding BrowserNavigated}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</WebBrowser>
This way, you can handle the Uri as parameter of the command:
<WebBrowser Name="wbGoogle">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Navigated" >
<i:InvokeCommandAction Command="{Binding BrowserNavigated}"
CommandParameter="{Binding Source, ElementName=wbGoogle}" />
</i:EventTrigger>
</i:Interaction.Triggers>
</WebBrowser>
Of course, this only lets you access the Uri of the page in the WebBrowser, not the page itself. If you need to bind to the page object itself, you need to extend the WebBrowser, with an attached property, that makes the Document property bindable. This is quite straight-forward:
Attached Document property for WebBrowser
After attaching this property to your WebBrowser, you can define the bindings of the WebBrowser just as in the above code, just with the attached property, instead of the Source property.
Note, that the syntax for binding to an attached property would be:
{Binding Path=(WebBrowserExtentions.Document)}
MVVM can be great for data binding and by using toolkits like MVVMLight, events that deal with user interactions can also be neatly dealt with.
However sometimes, controls like WebBrowserControl or ApplicationBar present a challenge to this. They can be difficult or impossible to bind with event triggers, or have complex behaviours. In these cases it is simpler if you handle the process of getting information from the control in your View code behind and send a simple message down to the VM.
Sure you could create an event that updates a property, write an Attached Property, or maybe use a 3rd party library; and there are cases that warrant that approach.
In your example I personally would use code-behind to handle the Navigated event and send down a message (or a method call on your VM) containing everything the VM wants in one go.
For instance:
private void wbGoogle_Navigated(object sender, System.Windows.Navigation.NavigationEventArgs e)
{
var vm = (TypeOfMyViewModel) this.DataContext;
//... read your HTML, get URL etc ...
vm.WebBrowserNavigatedTo(url, html, loadTime);
}
Similarly if an event raised from your VM would cause many things to happen in your View there comes a point where it is simpler to send a message or event to your View and let the View update the controls in code.
The key thing is keep the roles of MVVM distinct, e.g. to avoid a direct dependency of the ViewModel on the View. Interfaces can help here well as Messaging that comes with MVVMLight and its alternatives.
Related
I am trying to program in MVVM and I have the following use case:
A TextBox's text is bound to a property in the VM
A Button is command bound to a relay command
When the user presses the Button, the web browser's Navigate(url) method is called with the URL being the text in the TextBox
Above is the use case I want to create, but 1 and 2 is possible using the MVVM design pattern, but I could not find an adequate way to invoke the browser's Navigate() method. First of all, is it possible to call a method of a control from VM (please let me know if there is a way)? And in the above use case, what would be the appropriate way to structure the program if it is not possible?
Thanks
You could do the following:
Add a property MyUrl to your ViewModel
Bind MyUrl to your WebBrower's Source property
Make sure the property implements INotifyPropertyChanged. Then your Xaml:
<WebBrowser Source="{Binding MyUrl}" />
What if you REALLY wanted to call a UI method from the ViewModel?
If you ever do run into a situation where you absolutely need to call a method on a UI control for instance, you can hook up events on the ViewModel and then your UI registers to this event and does something UI specific...
VM code...
//... some VM logic
EpicNavigateEvent(url) // raise event, allowing UI to handle how
In your code-behind on your view (this is the part where some MVVM purests freak), you could register the event:
myVm.Navigate += doSomeNavigation;
...
public void doSomeNavigation(string url)
{
// call Navigate
}
I've successfully used this approach for applications where we have a single ViewModel layer and multiple technologies hooked up the views (WinForms, WPF and Asp.Net).
If you're looking for something more elegant, have a look at the User Interaction Patterns on MSDN.
The concept is the same though: Call something on the VM and the View is handles it appropriately.
Common scenarios for this type of approach is want to show a message to the user from the VM. Your VM should raise an event saying: ShowMyMessage("You're awesome"), then your UI is notified and handles it: MessageBox.Show(msg) or whatever.
As long as you stick to there rules you should be golden:
ViewModels should NOT be concerned about UI code
Views must ONLY handle the presentation of the data provided by your ViewModels.
Don't overcomplicate it. KISS...
I have a Grid with a ScrollViewer around it. At the top of my ScrollViewer is a Button. On a Click on the Button, I want the ScrollViewer to scroll to a Control at the bottom of the ScrollViewer.
With the following XAML I can bring the Control into view:
<Button Grid.Row="2" Content="Some Button" Command="{Binding DoJumpCommand}" CommandParameter="{Binding ElementName=window}"/>
The Command in the ViewModel is:
if (parameter is MainWindowView)
{
var mainWindowView = parameter as MainWindowView;
mainWindowView.myJumpTarget.BringIntoView();
}
This works fine. But I'm not sure if this is clean MVVM because I pass the complete View into the ViewModel.
Is there a better way to do this?
When I first saw your question, I thought that the general solution to handling events with MVVM is to handle them in an Attached Property. However, looking again, it occurred to me that you're not actually handling any events... you just want to call a method from a UI control. So really, all you need is a way to pass a message from the view model to the view. There are many ways to do this, but my favourite way is to define a custom delegate.
First, let's create the actual delegate in the view model:
public delegate void TypeOfDelegate();
It doesn't need any input parameters, because you don't need to pass anything from the view model to the view, except a signal... your intention to scroll the ScrollViewer.
Now let's add a getter and setter:
public TypeOfDelegate DelegateProperty { get; set; }
Now let's create a method in the code behind that matches the in and out parameters of the delegate (none in your case):
public void CanBeCalledAnythingButMustMatchTheDelegateSignature()
{
if (window is MainWindowView) // Set whatever conditions you want here
{
window.myJumpTarget.BringIntoView();
}
}
Now we can set this method as one (of many) handlers for this delegate in a Loaded event handler in the view code behind:
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Assumes your DataContext is correctly set to an instance of YourViewModel
YourViewModel viewModel = (YourViewModel)DataContext;
viewModel.DelegateProperty += CanBeCalledAnythingButMustMatchTheDelegateSignature;
}
Finally, let's call our delegate from the view model... this is equivalent to raising the event:
if (DelegateProperty != null) DelegateProperty(dataInstanceOfTypeYourDataType);
Note the important check for null. If the DelegateProperty is not null, then all of the attached handler methods will be called one by one. So that's it! If you want more or less parameters, just add or remove them from the delegate declaration and the handling method... simple.
So this is an MVVM way to call methods on a UI control from a view model. However, in your case it could well be argued that implementing this method would be overkill, because you could just put the BringIntoView code into a basic Click handler attached to your Button. I have supplied this answer more as a resource for future users searching for a way to actually call a UI method from a view model, but if you also chose to use it, then great!
Sometimes, i need to do some calls from View to VM. I know, that it is not MVVM style, but still. Should i always re-check DataContext to be my VM (in case it can be changed by re-activation from tombstoning or something like that), or it is enough to store in once?
var vm = DataContext as MyViewModel;
if (vm == null) return;
vm.DoSomething();
DataContext is set via Mvvm Light Locator
DataContext="{Binding MyViewModel, Mode=OneWay, Source={StaticResource ViewModelLocator}}"
It always is safe, when it's limited to a page. Whether in constructor, Loaded event handler or OnNavigatedTo, you can create objects and never check them again - either the page was kept in memory with all the objects, or the constructor, Loaded and OnNavigatedTo were called again upon re-activating from tombstoning. Problems with tombstoning occur mostly in cases of one page relying on the fact, that other page created something.
I use that sometimes, like that:
private MyViewModel viewModel;
When is the page loaded, then add value to that field
viewModel = (MyViewModel)DataContext;
Than I can use it anytime in my view after it.
I know it is not MVVM, but still ;)
I would like to learn the most proper way to go about this: I have a Listview in my GameView that is bound to an ObservableCollection<Adventurer>. Upon double-clicking on a cell, I need a new window (or something else if anything is more appropriate) to open and display data about the correct Adventurer according to the cell. So far I haven't been able to. This is what I have so far (it's not much, but nothing I've tried has worked).
The trigger/command in my ListView in GameView.xaml
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseDoubleClick">
<cmd:EventToCommand Command="{Binding Mode=OneWay, Path=ShowAdvCommand}"
CommandParameter="{Binding ElementName=AdvListView,
Path=SelectedItem}"
PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
And the command in GameViewModel.cs
ShowAdvCommand = new RelayCommand<Adventurer>(p =>
{
System.Windows.MessageBox.Show(p.Name);
});
The MessageBox is just there to confirm that Eventtocommand was working.
I essentially need a container that will take in the correct Adventurer as a parameter after double-clicking a Listview cell and allow me to display data specific to that instance. I would also prefer to stick to something MVVM-friendly.
Any advice would be greatly appreciated.
Update: I may have made a little progress:
GameViewModel:
ShowAdvCommand = new RelayCommand<Adventurer>(p =>
{
AdventurerView adv = new AdventurerView(p);
adv.Show();
});
AdventurerView:
public partial class AdventurerView : Window
{
Adventurer adv;
public AdventurerView(Adventurer adv)
{
this.adv = adv;
InitializeComponent();
}
}
Now I need to figure out how to make this work in XAML, databinding and such.
Update: ...and then I realized that this completely goes against MVVM. Does anybody have any advice?
Update: Would MVVM Light's messenger help me here? I've been tinkering with it but haven't gotten it to work.
Update: This question is still up in the air. I tried the Prism approach but there was some conflict between Prism and MVVM Light that caused more trouble than it was worth. I'm open to any ideas that are compatible with MVVM Light and the MVVM pattern in general.
Update: Also, I would like to do this in a way where multiple popups can exist concurrently, if possible.
In a similar situation, I've used MvvmLight's Messenger, and it worked really well. On double click, send a message from your viewmodel containing the entity you want to pass. Somewhere you will need to register to receive the message, depending on how you have set up your views and viewmodels to be activated.
You could register to receive the message in your MainPage.xaml, and either pass the entity straight to the view's constructor, or access the view's DataContext via an interface to pass the entity, depending on whether you're using a viewmodel in you childwindow. E.g.
AdventurerView adv = new AdventurerView();
IEntityViewModel vm = adv.DataContext as IEntityViewModel;
vm.SetCurrentEntity(entity);
adv.Show();
The IEntityViewModel might look like the following:
public interface IEntityViewModel<T> where T : class
{
void SetCurrentEntity(T entity);
}
The viewmodel would implement this interface:
public class AdventurerViewModel : IEntityViewModel<Adventurer>
{
public void SetCurrentEntity(Adventurer entity)
{
// Do what you need to with the entity - depending on your needs,
// you might keep it intact in case editing is cancelled, and just
// work on a copy.
}
}
As you've pointed out, proper MVVM wouldn't instantiate the view and pass the view model in through the constructor. You'd be better off binding the ViewModel to the View and there are many different ways of doing it.
One pattern that has emerged is a concept known as a "screen conductor". This is a top level ViewModel or controller that handles which ViewModel represents the main window. Again, many different ways to do this. For example, the ViewModel could raise a standard .net event that the Screen Conductor handles. You could use an message passing system like Caliburn.Micro's EventAggregator or MVVM Light's Messenger. I think MEFedMVVM also has an event aggregator to accomplish this as well.
Caliburn.Micro also has a WindowManager that you can pass in your ViewModel and have it automatically find the corresponding View and manage the window lifetime.
Lots of options. Find the one that works the best for you.
This is a nice case for Prism's InteractionRequest. Essentially, you have an InteractionRequest object on your ViewModel that you raise when you double click (inside your double click command). Your view has an Action on it that handles the Raised event and shows the new view. You pass a new ViewModel to that interaction and that's the DataContext for the window that'll display. Here's some good information to get you started. This is how I display all child windows in my application.
I'm using Prism and my views implement IConfirmNavigationRequest in order to enable them to perform validations and cancel the navigation if required.
My problem is that I have several views which don't use MVVM, and define DataContext = this. Doing so causes Prism to call my view's ConfirmNavigationRequest() twice, which means I ask for the user's response twice.
Basically what's going on is this:
Prism checks if the view implements IConfirmNavigationRequest and calls ConfirmNavigationRequest() on it if it does.
The user is asked whether he'd like to continue.
The user clicks OK and ConfirmNavigationRequest() returns true.
Prism checks if the viewmodel (in my case, it's the view again) implements IConfirmNavigationRequest and calls ConfirmNavigationRequest() on it if it does.
The user is asked again whether he'd like to continue.
As you can see, Prism asks my view for confirmation twice because it queries both the view and the viewmodel.
So my question is, how can I prevent this from happening or how can I detect which call is which so I can ignore one of them? I thought about investigating the continuationCallback parameter, but I don't like this solution so much since it's not unlikely it'll break in the next versions of Prism.
The best solution I got so far is the one I got from DCherubini at Prism's forum, which suggests that I won't set the view's DataContext on my UserControl, but use an inner element that will hold the view, and set the DataContext for it instead:
<UserControl>
<Grid x:Name="grid">
...
</Grid>
</UserControl>
grid.DataContext = this;
instead of
<UserControl x:Name="uc">
</UserControl>
uc.DataContext = this;
This should work, but it means I need to change each view individually. A solution that doesn't require making changes to the views would be nicer.