Advice on Views navigation using Caliburn.Micro MVVM WPF - c#

I'm new on Caliburn Micro and want some advice on which path to take to devolop my app interface and navigation between views.
My idea is to have a MainWindow which will contain a menu of buttons, each one related with a specific view. Each view will be stored in a separated WPF UserControl. The mainWindow will also contain a TabControl bound to an ObservableCollection of tabs on viewmodel. Everytime a button on menu is clicked, I want to add a new tab with a ContentPresenter inside that will dynamically load a view and its corresponding viewmodel.
So my questions:
1) Should I use a Screen Collection here?
2) Should the UserControl implement Screen interface?
3) How do I tell MainWindow ViewModel which view to load on the new added tab maintaining viewmodels decoupled?
Thanks to everyone in advance.
UPDATE
After a lot of reading and some help of the community I managed to resolve this. This is the resultant AppViewModel:
class AppViewModel : Conductor<IScreen>.Collection.OneActive
{
public void OpenTab(Type TipoVista)
{
bool bFound = false;
Screen myScreen = (Screen)Activator.CreateInstance(TipoVista as Type);
myScreen.DisplayName = myScreen.ToString();
foreach(Screen miItem in Items)
{
if (miItem.ToString() == myScreen.ToString())
{
bFound = true;
ActivateItem(miItem);
}
}
if (!bFound) ActivateItem(myScreen);
}
public ObservableCollection<MenuItem> myMenu { get; set; }
public ObservableCollection<LinksItem> myDirectLinks { get; set; }
public ICommand OpenTabCommand
{
get
{
return new RelayCommand(param => this.OpenTab((Type) param), null);
}
}
public AppViewModel()
{
OpenTab(typeof(ClientsViewModel));
MenuModel menu = new MenuModel();
myMenu = menu.getMenu();
myDirectLinks = menu.getLinks();
}
public void CloseTab(Screen param)
{
DeactivateItem(param, true);
}
}
I have to keep the ICommand from OpenTabCommand because the name convention of Caliburn.micro doesn't seems to work inside DataTemplate. Hope it could help someone else. Thanks to all

I've done something very similar using Caliburn.Micro, and based it on the SimpleMDI example included with the examples, with a few tweaks to fit my needs.
Much like in the example, I had a main ShellViewModel:
public class ShellViewModel : Conductor<IScreen>.Collection.OneActive
{
}
with a corresponding ShellView containing a TabControl - <TabControl x:Name="Items">, binding it to the Items property of the the Conductor.
In this particular case, I also had a ContextMenu on my ShellView, bound (using the Caliburn.Micro conventions), to a series of commands which instantiated and Activated various other ViewModels (usually with a corresponding UserControl, using the ActivateItem method on the Conductor.
public class YourViewModel: Conductor<IScreen>.Collection.OneActive
{
// ...
public void OpenItemBrowser()
{
// Create your new ViewModel instance here, or obtain existing instance.
// ActivateItem(instance)
}
}
In that case, I didn't require the ViewModels to be created with any particular dependency, or from any other locations in the program.
At other times, when I've needed to trigger ViewModel from elsewhere in the application, I've used the Caliburn.Micro EventAggregator to publish custom events (e.g. OpenNewBrowser), which can be handled by classes implementing the corresponding interface (e.g. IHandle<OpenNewBrowser>), so your main ViewModel could have a simple Handle method responsible for opening the required View:
public class YourViewModel: Conductor<IScreen>.Collection.OneActive, IHandle<OpenNewBrowser>
{
// ...
public void Handle(OpenNewBrowser myEvent)
{
// Create your new ViewModel instance here, or obtain existing instance.
// ActivateItem(instance)
}
}
This section of the documentation will probably be useful, especially the Simple MDI section.
Additional code I mentioned in the comments:
I sometimes use a generic method along these lines ensure that if I have an existing instance of a screen of a particular type, switch to it, or create a new instance if not.
public void ActivateOrOpen<T>() where T : Screen
{
var currentItem = this.Items.FirstOrDefault(x => x.GetType() == typeof(T));
if (currentItem != null)
{
ActivateItem(currentItem);
}
else
{
ActivateItem(Activator.CreateInstance<T>());
}
}
Used like:
public void OpenBrowser()
{
this.ActivateOrOpen<BrowserViewModel>();
}

Related

c# WPF MVVM TabControl with Multiple ViewModels and changing tabs

So I currently have a Window with a TabControl. The MainWindow has its own ViewModel and all the TabItems have their own ViewModels also.
I can easily change tabs from the MainWindow ViewModel through a bound SelectedIndex property. What I would like to do is change to another tab from code that runs within ANOTHER tab viewmodel. Since the Tabs are not part of the MainWindowViewModel, I am looking for a clean way to change the Tab without resorting to code behind to do it.
There are also cases, where I might need to change the tab from something such as a message prompt. I thinking my only way is to create and event and subscribe to that from MainWindowViewModel.
So I solved this with an EventAggregator.
public static class IntAggregator
{
public static void Transmit(int data)
{
if (OnDataTransmitted != null)
{
OnDataTransmitted(data);
}
}
public static Action<int> OnDataTransmitted;
}
First ViewModel sends data.
public class ModifyUsersViewModel : INotifyPropertyChanged
{
private void change_tab(int data)
{
IntAggregator.Transmit(data);
}
}
Second ViewModel receives data and then does something with it.
public class MainWindowViewModel : INotifyPropertyChanged
{
private int _Tab_SelectedIndex = 0;
public int Tab_SelectedIndex
{
get
{
return _Tab_SelectedIndex;
}
set
{
_Tab_SelectedIndex = value;
OnPropertyChanged(new PropertyChangedEventArgs("Tab_SelectedIndex"));
}
}
public MainWindowViewModel()
{
IntAggregator.OnDataTransmitted += OnDataReceived;
}
private void OnDataReceived(int data)
{
Tab_SelectedIndex = data;
}
}
Rather than trying to bind to SelectedIndex, if the TabItems have their own view models, then you can create a property for each of those view models: IsSelected and then bind the TabItem.IsSelected property to that:
<TabItem IsSelected="{Binding IsSelected}">
This prevents the view models from needing to know the index of their corresponding TabItem, something I would argue is a detail that should be specific to the view and something the view model should not concern itself with. What if you add another TabItem or want to change the order? Now you've got changes to make in the view models for something that could be just simple change to the view.

UITableView to ObservableCollection binding breaks when the containing UIViewController is initialised for the second time

I'm using mvvmcross and xamarin to bind an ObservableCollection to a UITableView. The collection is updated in place using the Add, Remove and Move methods. These calls correctly trigger INotifyCollectionChanged events and the TableView is updated as expected the first time the view containing the table is shown. If the user navigates away from the original view as part of the normal application flow but later returns the correct data is loaded into the table but calls to add, move and remove no longer update the table.
The INotifyCollectionChanged events are still being fired when the collection is updated
If I manually subscribe to these events in my subclass of MvxStandardTableViewSource and try and call ReloadData on the UITableView still does not update
My presenter is creating a new instance of the viewmodel and view each time the page is visited.
I'm also using Xamarin-Sidebar (https://components.xamarin.com/view/sidebarnavigation) for navigation in my application with a custom presenter to load the views but as far as I can tell the view is initialised via exactly the same code path whether it's the first or subsequent visit.
My presenters Show() method looks like this:
public override void Show(MvxViewModelRequest request)
{
if (request.PresentationValues != null)
{
if(NavigationFactory.CheckNavigationMode(request.PresentationValues, NavigationFactory.ClearStack))
{
MasterNavigationController.ViewControllers = new UIViewController[0];
base.Show(request);
}
else if(NavigationFactory.CheckNavigationMode(request.PresentationValues, NavigationFactory.LoadView))
{
var root = MasterNavigationController.TopViewController as RootViewController;
var view = this.CreateViewControllerFor(request) as UIViewController;
root.SidebarController.ChangeContentView(view);
}
}
else
{
base.Show(request);
}
}
The binding in my ViewController looks like this:
public override void ViewDidLoad()
{
base.ViewDidLoad();
View.AutoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight;
var source = new TracksTableSource(TableView, "TitleText Title; ImageUrl ImageUrl", ViewModel);
TableView.Source = source;
var set = this.CreateBindingSet<TracksViewController, TracksViewModel>();
set.Bind(source).To(vm => vm.PlaylistTable);
set.Apply();
}
And my viewmodel is as below where PlaylistTable is a subclass of ObservableCollection with the Update method using add, move and remove to keep the collection up to date.
public class TracksViewModel : MvxViewModel
{
private readonly IPlaylistService _playlistService;
private readonly IMessengerService _messengerService;
private readonly MvxSubscriptionToken _playlistToken;
public PlaylistTable PlaylistTable { get; set; }
public TracksViewModel(IPlaylistService playlistService, IMessengerService messengerService)
{
_playlistService = playlistService;
_messengerService = messengerService;
if (!messengerService.IsSubscribed<PlaylistUpdateMessage>(GetType().Name))
_playlistToken = _messengerService.Subscribe<PlaylistUpdateMessage>(OnDirtyPlaylist, GetType().Name);
}
public void Init(NavigationParameters parameters)
{
PlaylistTable = new PlaylistTable(parameters.PlaylistId);
UpdatePlaylist(parameters.PlaylistId);
}
public async void UpdatePlaylist(Guid playlistId)
{
var response = await _playlistService.Get(playlistId);
PlaylistTable.Update(new Playlist(response));
}
private void OnDirtyPlaylist(PlaylistUpdateMessage message)
{
UpdatePlaylist(message.PlaylistId);
}
}
This setup works perfectly the first time the view is initialised and updates the table correctly, it's only the second and subsequent times the view is initialised that the table fails to update. Can anyone explain why the binding fails when it appears the view is created using the same techniques in both instances?
I can post additional code if required but I believe the issue will be how I'm using the presenter since the code I've not posted from PlaylistTable functions correctly in unit tests and on first viewing.

How to load ViewModel/View in WPF under CaliburnMicro

I have a timer in my wpf application wich every 5 minutes ask a WCF service. If the service have message for my application, I get a list which contains text data and a specific code.
This code give an information about the view which must be loaded to print the data.
I have two ViewModel (the data source is the same for both): One for a Ticker > one view and One for Popup > two view
Project files :
View
Popup
PopHighView.xaml
PopMediumView.xaml
Ticker
TickerLayout.xaml
TickerNormal.xaml
ViewModel
PopViewModel
TickerViewModel
Models
AlertModel.cs
ViewParsers
AlertParser.cs
Datasource :
public class AlertParser : IAlertParser{
AlertServiceClient service;
public List<AlertModel> TickerAlertData()
{
try
{
service = new AlertServiceClient();
List<AlertModel> items = (from item in service.GetActiveAlert() select new AlertModel
{
Alertid= item.AlertId,
Alertstartdate = item.AlertStartDate,
Alerttitle = item.AlertTitle,
Alerttxt = item.AlertText
}).ToList();
return items;
}
catch
{
return null;
}
}
}
When my application is launched, there is no loaded view, only a icon in the systray(with wpf notifyicon).
My problem is, under theses circonstances, I don't understand how I could loaded a couple ViewModel/View, and pass the data to them, when my timer return a true from my service.
Many examples on the web have a mainview loaded, that's why I'm lost (like Conductor example on caliburn micro page).
Thanks for any help !
Edit :
Ok, My timer look like that :
if (service.IsAlertReady()=true)
{
string hostName = Dns.GetHostName();
string myIP = Dns.GetHostEntry(hostName).AddressList[0].ToString();
service.IsAlertForMe(myIP);
if(service.IsAlertForMe(myIP) == true)
{
ShellViewModel shell = new ShellViewModel();
shell.ShowMediumPop();
}
else
...
ShellViewModel
public class ShellViewModel : Conductor<object>
{
public void ShowMediumPop()
{
ActivateItem(new PopViewModel());
}
}
PopViewModel
public class PopViewModel : screen
{
protected override void OnActivate()
{
base.OnActivate();
}
}
PopView.Medium
<UserControl x:Class="TerminalClientProto.View.PopView"
...
cal:View.Model="{binding}"
cal:View.Context="Medium"
>
I'm sorry but I don't understand how I could launch my view when my Ticker tick. I've read the documentation, but I need some hints to understand this mechanism.
A program, any program, including the very program that contains the views you want to display can show a view in a number of ways. Here's a few:
var app = new App();
app.InitializeComponent();
app.Run();
Or you can start the view directly:
var view = new MyView();
view.Show();
// or
view.ShowDialog();
If the view is a MainWindow, then you can create a ContentControl area within the view to inject the Usercontrol containing the sub-view of what you want displayed. This still requires the MainWindow to be open... So the examples above would also work when injecting UserControls into a MainWindow. The act of injecting a User control is setting the ContentControl's Content to an instance of the User Control itself. Eventhandlers can handle this scenario nicely...
public void NewUserControlInMainWindow(object sender, UserControl uc){
//XCC = the Xaml content control in main window
XCC.Content = uc;
}
I'm not really sure how Caliburn does view injection....

Using one view model in multiple windows

How can I use one view model for many windows in WPF? I need model to be updated from one window only and handle these changes in others (for ex. property 'Locked').
I have one view model incapsulating the most general info that should be used not only on the A (suppose it is 'Company') window but also on windows child window B (suppose it is 'Person'). So the 'General' view model should be determined by A entity but be passes to all children entity. While updating this view model on A window - we should se changes on all B windows.
public partial class A : WindowBase
{
private GeneralViewModel general;
public GeneralViewModel General
{
get
{
return this.general ?? (this.general = new GeneralViewModel ());
}
}
}
public partial class B : WindowBase
{
private GeneralViewModel general;
public GeneralViewModel General
{
get
{
return this.general ?? (this.general = new GeneralViewModel ());
}
}
public B(GeneralViewModel g)
{
this.general = g;
}
}
I wish the model should be updated only in A and B was simply displaying that changes were maid. In case I pass model as it is shown in this code or if I implement 'General' as property with getter and setter changes are not applied.
Thanks for any help.
You could use a singleton-class as your ViewModel.
Example:
public Window()
{
this.DataContext = ViewModel.Instance.
}
EDIT:
public GeneralViewModel
{
public DataType Model
{
get { return DataType.Instance; }
}
}
Now everytime you access the Model in one of your GeneralViewModels, it is locked for all others.
Initialise your view model in a static member somewhere and have the windows return the value as their GeneralViewModel.
In the scenario you mentioned, your GeneralViewModel is a kind of Dependency to both your Window classes and for these purposes you can use some of the available IoC containers or MEF built into .Net 4. You can register your Dependencies including the GeneralViewModel in some application startup event.
Below is some sample code that will make your GeneralViewModel instance to be then located from the container it is registered with (MEF in below case):
[Export(typeof(B))]
public partial class B : WindowBase
{
private GeneralViewModel general;
public GeneralViewModel General
{
get
{
return this.general ?? (this.general = new GeneralViewModel ());
}
}
[ImportingConstructor]
public B(GeneralViewModel g)
{
this.general = g;
}
}
To learn more about MEF,see these articles:
CodePlex
Ten Reasons to use the Managed Extensibility Framework
Managed Extensibility Framework Overview
An Introduction to Managed Extensibility Framework (MEF) - Part I
There are many other DI and IoC containers available as Open Source downloads.
There is no problem if you use MVVM. In this case your ViewModel will correspond to some View which is basically the UserControl and you can place it to as many Windows as you wish.
And When you implement MVVM you should also use INotifyPropertyChanged or ObservableCollections

How can I open a page and assign its datacontext

my question is relatively easy, I guess.
I have a page to display my data. On a click on a button I want to open a new page with the datacontext of an element 2 layers above the datacontext of the current element.
Explanation:
My ViewModel is a class (ViewModelContainer) that contains more ViewModels. One is a summary of values and one is the detailed view of that.
public class SummaryViewModel
{
public int somevalue; // is a property
public ObservableCollection<SummarizedItems> items; // is a property
}
public class DetailsViewModel
{
public int someOthervalue; // is a property
public int stuffA; // is a property
public int stuffB; // is a property
}
public class ViewModelContainer : ViewModelBase
{
private SummaryViewModel _sum;
public SummaryViewModel sum { } // is a property
private DetailsViewModel _det;
public DetailsViewModel det { } // is a property
}
The View where I can press a button is bound to the value of the ObservableCollection of SummaryViewModel.
Everything is fine till now. When I press the button, a new page, showing the details should be opened. I use an ICommand to handle the click, and pass it the details view as a CommandParameter.
<Button Name="OpenDetailsButton" Command="{Binding Path=ACommand}" CommandParameter="{DynamicResource Details}"
I define a page as a resource in the same file, where the datacontext is still ViewModelContainer.
<pages:DetailsViewPage DataContext="{Binding Path=det }" x:Key="Details"/>
The page opens, but the datacontext is not available. I get the following error:
System.Windows.Data Error: 3 : Cannot find element that provides DataContext.
Has anyone an idea how I could open the details view and providing the datacontext? I cannot move the DetailsViewModel to another class, because it is only possible to update it there.
Thanks
I have solved the problem by creating a helper to go up the visual tree and use the datacontext of the element i needed. Thanks for everybody who tried to help :)
the method looks like this:
public static UIELEMENT FindUiElementUpVisualTree(DependencyObject initial)
{
DependencyObject current = initial;
while (current != null && current.GetType() != typeof(UIELEMENT))
{
current = VisualTreeHelper.GetParent(current);
}
return current as UIELEMENT;
}
where UIELEMENT is the object you are looking for e.g. a window or a button or something.
usually views and viewmodels have a one to one relationship. In this case it seems like there is a many to one relationship. How about a DetailsPageViewModel?

Categories

Resources