Handling Navigation in MVVM WPF application - c#

I am developing a WPF application that follows MVVM. Now I am handling navigation of views in the following manner.
MainWindow View
<Border>
<StackPanel>
<local:Home
Content="{Binding CurrentView,Converter={StaticResource ViewConverterHome}, UpdateSourceTrigger=PropertyChanged}"/>
<local:Page1
Content="{Binding CurrentView,Converter={StaticResource ViewConverterPage1}, UpdateSourceTrigger=PropertyChanged}"/>
<local:Page2
Content="{Binding CurrentView,Converter={StaticResource ViewConverterPage2}, UpdateSourceTrigger=PropertyChanged}"/>
</StackPanel>
</Border>
Home, Page1,Page2 are 3 views. HomeVM,Page1VM,Page2VM are view models corresponding to the views. There is a class call ApplicationViewModel that contains a property CurrentView of type CViewModelBase which is the parent class for all three viewmodels. ApplicationViewModel handles the navigation in the folowing manner
private void OnUserInputNextClicked(object sender, OperationInformationChangedEventArgs e)
{
do
{
if (this.CurrentView is HomeVM)
{
this.CurrentView = null;
Page1VM page1 = new Page1VM("BNM", "MATH HONS", "13");
page1.NextCilcked += new EventHandler<OperationInformationChangedEventArgs>(OnUserInputNextClicked);
page1.BackCilcked += new EventHandler<OperationInformationChangedEventArgs>(OnUserInputBackClicked);
this.CurrentView = page1;
break;
}
if (this.CurrentView is Page1VM)
{
this.CurrentView = null;
Page2VM page2 = new Page2VM("Kolkata", "Monoj", "Itachuna");
page2.NextCilcked += new EventHandler<OperationInformationChangedEventArgs>(OnUserInputNextClicked);
page2.BackCilcked += new EventHandler<OperationInformationChangedEventArgs>(OnUserInputBackClicked);
this.CurrentView = page2;
break;
}
if (this.CurrentView is Page2VM)
{
this.CurrentView = null;
HomeVM home = new HomeVM("Anirban", "30");
home.NextCilcked += new EventHandler<OperationInformationChangedEventArgs>(OnUserInputNextClicked);
this.CurrentView = home;
break;
}
} while (false);
}
The navigation is working perfectly; But dispose of disappeared views are not getting called.So all the views live till the end. Is there any way to prevent that?

Your Views will always exist because you added a copy of each one to your UI with the XAML, even if the Content contained in them may not exist
Typically I will use a ContentControl to display content instead of creating an instance of the control for each content type, and I'll use DataTemplates to tell WPF how to draw each type of content.
For example,
<Window.Resources>
<DataTemplate DataType="{x:Type local:HomeVM}">
<local:Home Content="{Binding }" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:Page1VM}">
<local:Page1 Content="{Binding }" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:Page2VM}">
<local:Page2 Content="{Binding }" />
</DataTemplate>
</Window.Resources>
<Border>
<StackPanel>
<ContentControl Content="{Binding CurrentView}" />
</StackPanel>
</Border>
This way, you only have one instance of your Content in the VisualTree, and the DataTemplate WPF users to draw your content changes based on it's DataType.
I have an example of this kind of navigation with WPF on my blog if you're interested in checking out a full code sample

You need to change DataContext of MainWindow. It depends on your integration. When I make a MVVM application what I do is that pass MainWindow object to every view constructor. And whenever I have to move to next page (like on next button) I change the MainWindow object DataContext to new view.
Something like this.
public PageOneViewModel
{
private MainWindow _mainWindow;
public PageOneViewModel(MainWindow mainWindow)
{
// Here I am saving MainWindow object.
_mainWindow = mainWindow;
}
public OnNext()
{
// Here I am changing the view.
MainWindow.DataContext = new PageTwoViewModel(_mainWindow);
}
}

Have you considered using Frame?
<Frame Name="YourFrame" Navigated="OnNavigated"/>
and then you can call
YourFrame.CanGoBack(), YourFrame.GoBack()
etc.

Here's a link to my answer to a similar question with working source code. The technique I used is a little similar to Faisal's solution.
If you need a good downloadable sample solution that demonstrates navigation using a side menu, look at here and here(simpler example).

Related

AvalonDock MVVM Anchorable Location

I have an AvalonDock (version 3.5) in my MVVM WPF application. The binding is as follows:
<xcad:DockingManager Name="_dockingManager" Grid.Row="1"
DataContext="{Binding DockingManagerViewModel}"
DocumentsSource="{Binding Documents}"
AnchorablesSource="{Binding Anchorables}" >
So when I add a new Anchorable to the corresponding "Anchorables" collection in my view model the corresponding view shows up. Nevertheless the views always show up docked on the right side of my application. How can I control that the view is docked on the left side of my application via the view model?
I don't think you can control this in you viewmodel.
There 2 ways for controlling this.
You can restore the layout from a previously saved (default) layout whenever the
application is started for te first time or
You can setup the XAML to use an initial layout as you wish (prefered solution)
For the second option:
You can use the XAML binding in the DockingManager class to implement your requirement:
See TestApp sample for full implementation of the below snippet (just change LeftSide to BottomSide to see the effect):
<avalonDock:DockingManager Grid.Row="1">
...
<avalonDock:LayoutRoot.LeftSide>
<avalonDock:LayoutAnchorSide>
<avalonDock:LayoutAnchorGroup>
<avalonDock:LayoutAnchorable Title="AutoHide1 Content" ContentId="AutoHide1Content" IconSource="/AvalonDock.TestApp;component/Images/address-book--pencil.png" >
<TextBox Text="{Binding TestTimer, Mode=OneWay, StringFormat='AutoHide Attached to Timer ->\{0\}'}"/>
</avalonDock:LayoutAnchorable>
<avalonDock:LayoutAnchorable Title="AutoHide2 Content" ContentId="AutoHide2Content">
<StackPanel Orientation="Vertical">
<TextBox/>
<TextBox/>
</StackPanel>
</avalonDock:LayoutAnchorable>
</avalonDock:LayoutAnchorGroup>
</avalonDock:LayoutAnchorSide>
</avalonDock:LayoutRoot.LeftSide>
</avalonDock:LayoutRoot>
</avalonDock:DockingManager>
You can add a property (call it InitialPosition, or something like that) to your Anchorable view model, and implement a ILayoutUpdateStrategy to position the anchorable on the left, right or bottom side.
Add something like this to your XAML:
<xcad:DockingManager …>
…
<xcad:DockingManager.LayoutUpdateStrategy>
<local:LayoutUpdate />
</xcad:DockingManager.LayoutUpdateStrategy>
</xcad:DockingManager>
and your LayoutUpdate class:
class LayoutUpdate: ILayoutUpdateStrategy
{
static Dictionary<PaneLocation, string> _paneNames = new Dictionary<PaneLocation, string>
{
{ PaneLocation.Left, "LeftPane" },
{ PaneLocation.Right, "RightPane" },
{ PaneLocation.Bottom, "BottomPane" },
};
public bool BeforeInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorableToShow, ILayoutContainer destinationContainer)
{
if (anchorableToShow.Content is IAnchorable anch)
{
var initLocation = anch.InitialLocation;
string paneName = _paneNames[initLocation];
var anchPane = layout.Descendents()
.OfType<LayoutAnchorablePane>()
.FirstOrDefault(d => d.Name == paneName);
if (anchPane == null)
{
anchPane = CreateAnchorablePane(layout, Orientation.Horizontal, initLocation);
}
anchPane.Children.Add(anchorableToShow);
return true;
}
return false;
}
static LayoutAnchorablePane CreateAnchorablePane(LayoutRoot layout, Orientation orientation,
PaneLocation initLocation)
{
var parent = layout.Descendents().OfType<LayoutPanel>().First(d => d.Orientation == orientation);
string paneName = _paneNames[initLocation];
var toolsPane = new LayoutAnchorablePane { Name = paneName };
if (initLocation == PaneLocation.Left)
parent.InsertChildAt(0, toolsPane);
else
parent.Children.Add(toolsPane);
return toolsPane;
}
public void AfterInsertAnchorable(LayoutRoot layout, LayoutAnchorable anchorable)
{
// here set the initial dimensions (DockWidth or DockHeight, depending on location) of your anchorable
}
This code is extracted and changed a bit from the working application, with different types and names. It should probably work, but there might be a typo or other error somewhere.

ListView not updating on CollectionChanged

I am starting to play with Realm, and I am trying to bind a collection from the Realm database to a ListView. The binding works fine, but my ListView does not update when adding new items. My understanding is that IRealmCollection<> implements INotifyCollectionChanged and INotifyPropertyChanged events.
Here is a simple application to reproduce the issue:
View:
<Page x:Class="App3.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:App3"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<StackPanel>
<Button Click="ButtonBase_OnClick" Content="Add" />
<ListView x:Name="ListView">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Id}" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackPanel>
</Grid>
</Page>
CodeBehind:
namespace App3
{
public class Thing : RealmObject
{
public string Id { get; set; } = Guid.NewGuid().ToString();
}
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
private Realm _realm;
private IRealmCollection<Thing> things;
public MainPage()
{
this.InitializeComponent();
_realm = Realm.GetInstance();
things = (IRealmCollection<Thing>)_realm.All<Thing>();
ListView.ItemsSource = things;
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
_realm.Write(() =>
{
var thing = new Thing();
_realm.Add(thing);
});
}
}
}
I normally use MVVM (Template10), but this is a simple application to demonstrate the issue. Clicking the Add button adds an item to the database, but the ListView only updates when the application is first loaded. I have read similar questions, but I have not been able to find an answer that works yet. Inverse Relationships and UI-Update not working? is the closest I have found yet, but still does not fix the issue.
EDIT
I can force it to rebind like so:
ListView.ItemsSource = null;
ListView.ItemsSource = things;
But that is not optimal. I am trying to take advantage of Realm's "live objects" where the collection should always know when items are changed or added.
EDIT 2
Setting BindingMode=OneWay in code-behind also does not change the behavior:
_realm = Realm.GetInstance();
things = (IRealmCollection<Thing>)_realm.All<Thing>();
var binding = new Binding
{
Source = things,
Mode = BindingMode.OneWay
};
ListView.SetBinding(ListView.ItemsSourceProperty, binding);
SOLUTION
It turned out to be a known issue in IRealmCollection: https://github.com/realm/realm-dotnet/issues/1461#issuecomment-312489046 which is fixed in Realm 1.6.0. I have updated to the pre-release NuGet package and can confirm that the ListView now updates as expected.
Set Mode=OneWay in Binding
Method 1: In Xaml
<ListView ItemsSource="{x:Bind things, Mode=OneWay}" />
Method 2: In Code Behind
Binding myBind = new Binding();
myBind.Source = things;
myBind.Mode = BindingMode.OneWay;
myListView.SetBinding(ListView.ItemsSourceProperty, myBind);
It is a bug in IRealmCollection. You can use Prerelease Nuget to solve it.
For more info:
IRealmCollection does not update UWP ListView
GitHub Issue: IRealmCollection does not update UWP ListView

Using MessagingCenter for communicating between ViewModels to Views and Navigation

In a XAML Page (A View), I have a bunch of controls like Pickers that are binded to a ViewModel, called "NegotiationVM"
For example, I have a Picker binded to one of those properties "NegotiatorSelected" which returns an object "Negotiator".
My corcern is about a pair of buttons beside of that negotiator picker, are not binded to that Negotiation ViewModel or any Command, those buttons are for Adding or Editing a Negotiator. Since I'm trying to follow MVVM pattern (with no framework yet...) I would like to know what is the best practice here.
My approach is in the First ViewModel create Commands that calls methods that send an instance of the child ViewModel to the View, so when this receive it, then navigates to the child View.
public class NegotiationVM: INotifyPropertyChanged{
public NegotiationVM{
AddNegotiatorCommand = new Command(AddNegotiator,()=> !IsBusy);
}
...
public void AddNegotiator(){
negotiatorvm = new NegotiatiorVM(_client)
MessagingCenter.Send<NegotiationVM,NegotiatorVM>(this,"NegotiatiorVM",negotiatiorvm)
}
public void EditNegotiator(){
negotiatorvm = new NegotiatiorVM(_client,_negotiatior)
MessagingCenter.Send<NegotiationVM,NegotiatorVM>(this,"NegotiatiorVM",negotiatiorvm)
}
}
So in NegotiationView I do this:
<!-- More controls binded to NegotiationVM -->
...
<!-- -->
<Picker x:Name="pickerNegotiator" Title="Select..." ItemsSource="{Binding Negotiatorsofthisnegotiation}" SelectedItem="{Binding SelectedNegotiator}" />
<Button x:Name="btnAddNegotiator" Text="➕" BackgroundColor="#3276b1" TextColor="White" HorizontalOptions="End" WidthRequest="35" Command="{Binding AddNegotiatorCommand}" />
<Button x:Name="btnEditNegotiator" Text="📝" BackgroundColor="#3276b1" TextColor="White" HorizontalOptions="End" WidthRequest="35" Command="{Binding EditNegotiatorCommand}" />
<!-- ... -->
and in code-behind:
...
public NegotiationView(){
negotiationvm = new NegotiationVM();
this.BindingContext = negotiationvm;
MessagingCenter.Subscribe<NegotiationVM,NegociatorVM>(this,"NegotiatorVM",AddEditNegotiator);
}
...
void AddEditNegotiator(NegotiationVM arg1, NegotiatorVM vm)
{
NegotiatorPage negotiatorPage = new NegotiatorPage()
{
BindingContext = vm
};
Navigation.PushAsync(negotiatorPage);
}
So using MessagingCenter to communicate a ViewModel to a View in order to bind it to a child view and then Navigate to its view, is this a good practice considering that I'm not using any MVVM framework? Is this approach is making ViewModel from View decoupled enough? I read some comments that using MessagingCenter is not a good idea.

Keep a reference to objects passed to a UserControl

I created a UserControl that has a ContentControl in it. This ContentControl gets Buttons from the normal .xaml-pages. But depending on some events I need to change this Button's Label or Image but i am getting a NullReferenceException.
UserControl1.xaml
<Grid>
<!-- different Stuff that needs to be around -->
<ContentControl Content="{Binding UserControlContent, ElementName=userContent}"/>
</Grid>
UserControl1.xaml.cs
public static readonly DependencyProperty AppBarContentProperty =
DependencyProperty.Register("UserControlContent", typeof(Grid), typeof(UserControl1), new PropertyMetadata(new Grid()));
public Grid UserControlContent
{
get { return (Grid)GetValue(UserControlContentProperty); }
set { SetValue(UserControlContentProperty, value); }
}
MainPage.xaml
<local:UserControl1>
<local:UserControl1.UserControlContent>
<Grid>
<Controls:RoundButton x:Name="btn1"/>
</Grid>
</local:UserControl1.UserControlContent>
</local:UserControl1>
MainPage.xaml.cs
MainPage()
{
btn1.Label = "new label";
}
As soon as I try this with a button inside of the UserControl it fails. With buttons that stay outside it works.
Is there any deeper binding possible to keep control of these buttons?
The trick is using the mvvm-binding!
The button's values are bound now:
Label="{Binding RoundButtons[3].Label}"
Visibility="{Binding RoundButtons[3].VisibilityState, FallbackValue=Visible}"
This allows me to define default-values and still change them on the fly as I need them to be changed.
Hope someone needs this information ;)

CaliburnMicro Action does not fire inside data template

I want to create some sort of filter, when user clicks the filter button from the app bar it will fire up a popup page with list picker in it. I've googled and tried quite a number of solutions but still cannot get it to work.
Here are my codes:
XAML (MainPageView.xaml)
<phone:PhoneApplicationPage.Resources>
<DataTemplate x:Key="PivotContentTemplate">
<phone:Pivot Margin="-12,0,0,0" Title="FOREX NEWS" Height="672">
<phone:PivotItem Header="filter" FontFamily="{StaticResource PhoneFontFamilySemiLight}" FontSize="32">
<StackPanel Margin="12,0,0,0">
<toolkit:ListPicker Header="currencies" SelectionMode="Multiple"
micro:Message.Attach="[Event SelectionChanged] = [Action OnCurrenciesChanged($eventArgs)]">
<sys:String>gbp</sys:String>
<sys:String>eur</sys:String>
<sys:String>usd</sys:String>
</toolkit:ListPicker>
</StackPanel>
</phone:PivotItem>
</phone:Pivot>
</DataTemplate>
<phone:PhoneApplicationPage.Resources>
...
Still inside MainPageView.xaml
<bab:BindableAppBar Grid.Row="2" Mode="Minimized">
<bab:BindableAppBarButton micro:Message.Attach="[Event Click] = [Action ShowFilter($view, $eventArgs]">
</bab:BindableAppBarButton>
</bab:BindableAppBar>
MainPageViewModel.cs
public void ShowFilter(object sender, RoutedEventArgs e)
{
var view= sender as MainPageView;
CustomMessageBox messageBox = new CustomMessageBox()
{
ContentTemplate = (DataTemplate)view.Resources["PivotContentTemplate"],
LeftButtonContent = "filter",
RightButtonContent = "cancel",
IsFullScreen = true // Pivots should always be full-screen.
};
messageBox.Dismissed += (s1, e1) =>
{
switch (e1.Result)
{
case CustomMessageBoxResult.LeftButton:
// Do something.
break;
case CustomMessageBoxResult.RightButton:
// Do something.
break;
case CustomMessageBoxResult.None:
// Do something.
break;
default:
break;
}
};
messageBox.Show();
}
public void OnCurrenciesChanged(SelectionChangedEventArgs e)
{
}
For your information, I am using Caliburn.Micro and WP Toolkit for the CustomMessageBox and ListPicker.
I received exception No target found for method OnCurrenciesChanged. I only receive the exception when I after I select few items in the list picker and click any of the buttons to save the change. Another thing is that the OnCurrenciesChanged does not get triggered at all.
I think (based on what I read so far) whenever the CustomMessageBox get called, the datacontext its operating at is no longer pointing to the MainPageViewModel thus it could not find the method. But I am not sure how to actually do this.
More details:
Exception happen after I click the left button (checkmark)
Updates
So far I have try the following:
<StackPanel Margin="12,0,0,0" DataContext="{Binding DataContext, RelativeSource={RelativeSource TemplatedParent}}"> //also tried with Self
and I also added this when I instantiate messageBox
var messageBox = new CustomMessageBox()
{
ContentTemplate = (DataTemplate)view.Resources["PivotContentTemplate"],
DataContext = view.DataContext, // added this
LeftButtonContent = "filter",
RightButtonContent = "cancel",
IsFullScreen = true
};
The idea is that when the messsagebox is created, the datacontext will be the same as when the view is instantiated. However, it seems that the datacontext does not get inherited by the PickerList
Ok so I managed to get this to work. The solution is not pretty and I think it beats the purpose of MVVM at the first place.
Based on http://wp.qmatteoq.com/first-steps-in-caliburn-micro-with-windows-phone-8-how-to-manage-different-datacontext/ , inside a template the DataContext will be different. So, I need to somehow tell ListPicker to use the correct DataContext.
The solution provided by link above doesn't work for me. I think it is because when ListPicker is called inside CustomMessageBox, MainPageViewModel is no longer available or it seems not to be able to find it as suggested by the exception. So as per above code example in the question, even if I set the correct DataContext to the CustomMessageBox, it does not get inherited somehow by the ListPicker.
Here is my solution:
var messageBox = new CustomMessageBox()
{
Name = "FilterCustomMessageBox", // added this
ContentTemplate = (DataTemplate)view.Resources["PivotContentTemplate"],
DataContext = view.DataContext,
LeftButtonContent = "filter",
RightButtonContent = "cancel",
IsFullScreen = true
};
In the XAML, I edited to this
<toolkit:ListPicker Header="currencies" SelectionMode="Multiple"
micro:Action.TargetWithoutContext="{Binding ElementName=FilterCustomMessageBox, Path=DataContext}"
micro:Message.Attach="[Event SelectionChanged] = [Action OnCurrenciesChanged($eventArgs)]">
It's ugly because both ViewModel and View need to explicitly know the Name. In WPF, you can just do something like this in the binding to inherit the DataContext of the parent/etc but this is not available for WP.
{Binding DataContext, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type DataGrid}}}
If anyone has better workaround, do let me know!

Categories