Why does ReactiveUI binding stop working? - wpf

I encountered a very strange issue and cannot understand how to solve it.
My application window has a page navigator on the right. When you click any page in the navigator this page is displayed on the main preview surface on the left. Here how it looks like:
In the background there are 3 ViewModels: Root VM for the main window, Navigator VM and Page VM.
The Navigator VM looks as follows:
public class PageNavigatorViewModel : ReactiveObject, IPageNavigatorViewModel
{
public PageNavigatorViewModel()
{
Pages = new ObservableCollectionExtended<IPageViewModel>();
AddEmptyPageCommand = ReactiveCommand.Create(AddEmptyPage);
SelectPageCommand = ReactiveCommand.Create(SelectPage);
}
public IObservableCollection<IPageViewModel> Pages { get; }
// Is bound to a button in UI:
public ReactiveCommand<Unit, Unit> AddEmptyPageCommand { get; }
// This command is executed when the user clicks on a page in the Page Navigation Panel:
public ReactiveCommand<IPageViewModel, Unit> SelectPageCommand { get; }
public IPageViewModel CurrentPage
{
get => _CurrentPage;
set => SetCurrentPage(value);
}
public void AddEmptyPage()
{
var page = CreatePage();
Pages.Add(page);
SetCurrentPage(page);
}
public void SelectPage(IPageViewModel page)
{
SetCurrentPage(page);
}
private IPageViewModel _CurrentPage;
private void SetCurrentPage(IPageViewModel page)
{
foreach (var p in Pages)
p.IsCurrent = false;
page.IsCurrent = true;
this.RaiseAndSetIfChanged(ref _CurrentPage, page, nameof(CurrentPage));
}
}
The VM of the main window is as follows:
public class MainViewModel : ReactiveObject, IMainViewModel
{
public RootViewModel()
{
PageNavigator = new PageNavigatorViewModel();
this.WhenPropertyChanged(vm => vm.PageNavigator.CurrentPage)
.Subscribe(vm => CurrentPage = vm.Value);
}
public IPageNavigatorViewModel PageNavigator { get; protected set; }
[Reactive] public IPageViewModel CurrentPage { get; protected set; }
}
And finally here the code in the main window:
public partial class MainWindow
{
public MainWindow()
{
InitializeComponent();
Initialize();
}
private void Initialize()
{
ViewModel = new RootViewModel();
// Add a few pages with random background:
for (var i = 0; i < 5; i++)
{
ViewModel.PageNavigator.AddEmptyPage();
ViewModel.PageNavigator[i].Background = GetRandomColor();
}
this.WhenActivated(d =>
{
// DrawingSurfaceBorder - it's ... just a standard WPF Border control:
this.OneWayBind(ViewModel, vm => vm.CurrentPage.Background, v => v.DrawingSurfaceBorder.Background).DisposeWith(d);
});
}
}
So, everything works as expected for those 5 pages added on VM initialization: I click a page and can see that the preview changes its color to the selected page. But when I add a new page, and in the code you can see this new page is Current now, however, the preview does not change at all, it's just ignores new added pages. Looks like the ReactiveUI does not bind new pages to the view. And I could not find anything on this issue on the web.

Wow, I've just found the answer accidentally! AddEmptyPage does not set the PageViewModel.Background property, so it is just null for all new pages. Looks like this breaks the binding, because setting this property fixed the issue. It is really hard to track mistake. I would be happy to have some exception on such case, but it's WPF - the most hard to debug framework in the Universe. I think I would kill myself even before my boss would lose his patience XD

Related

How to use ReactiveList so UI is updated when items are added/removed/modified

I'm creating a WinForms application with a DataGridView. The DataSource is a ReactiveList. Adding new items to the list however does not update the UI.
ViewModel
public class HomeViewModel: ReactiveObject
{
public ReactiveCommand<object> AddCmd { get; private set; }
ReactiveList<Model> _models;
public ReactiveList<Model> Models
{
get { return _models; }
set { this.RaiseAndSetIfChanged(ref _models, value); }
}
public HomeViewModel()
{
Models = new ReactiveList<Model>() { new Model { Name = "John" } };
AddCmd = ReactiveCommand.Create();
AddCmd.ObserveOn(RxApp.MainThreadScheduler);
AddCmd.Subscribe( _ =>
{
Models.Add(new Model { Name = "Martha" });
});
}
}
public class Model
{
public string Name { get; set; }
}
View
public partial class HomeView : Form, IViewFor<HomeViewModel>
{
public HomeView()
{
InitializeComponent();
VM = new HomeViewModel();
this.OneWayBind(VM, x => x.Models, x => x.gvData.DataSource);
this.BindCommand(VM, x => x.AddCmd, x => x.cmdAdd);
}
public HomeViewModel VM { get; set; }
object IViewFor.ViewModel
{
get { return VM; }
set { VM = (HomeViewModel)value; }
}
HomeViewModel IViewFor<HomeViewModel>.ViewModel
{
get { return VM; }
set { VM = value; }
}
}
The view always show "John".
Debugging Subscribe show added items.
Tried it with ObservableCollection same result.How to use ReactiveList so UI is updated when new items are added
Tried it with IReactiveDerivedList same result. Does ReactiveUI RaiseAndSetIfChanged fire for List<T> Add, Delete, Modify?
I think what you want is a ReactiveBindingList rather than a ReactiveList. This is a WinForms specific version of the ReactiveList for binding purposes.
You should use BindingList.
reference :
"If you are bound to a data source that does not implement the IBindingList interface, such as an ArrayList, the bound control's data will not be updated when the data source is updated. For example, if you have a combo box bound to an ArrayList and data is added to the ArrayList, these new items will not appear in the combo box. However, you can force the combo box to be updated by calling the SuspendBinding and ResumeBinding methods on the instance of the BindingContext class to which the control is bound."
https://learn.microsoft.com/en-us/dotnet/desktop/winforms/controls/how-to-bind-a-windows-forms-combobox-or-listbox-control-to-data?view=netframeworkdesktop-4.8
Or
ReactiveBindingList
It work fine for me. !!!

How to display new modal form in ReactiveUI 6.5

I am one of a team of developers currently maintaining a large suite of applications written using the WinForms UI.
In order to improve testability of our applications, we are wanting to move to an MVVM style, to separate the UI from the business logic. However, we need to keep using the WinForms UI, to minimize impact on our users as they work with different applications in the suite.
In trialing ReactiveUI, I have got a handle on how to bind form controls and commands to my view model, but cannot find documentation or examples on how to pop up a modal form to ask for or display additional information. For example these documentation pages on routing mention every supported UI framework except WinForms: http://docs.reactiveui.net/en/user-guide/routing/index.html, https://github.com/reactiveui/ReactiveUI/blob/docs/docs/basics/routing.md
Unfortunately, the ReactiveUI "good examples page" does not appear to have any WinForms-based examples, and all the other ReactiveUI / WinForms examples I can find using Google are only a single form.
I definitely want to keep forms/views out of the view model to maintain testability.
I believe the right way is to have a ReactiveCommand that is triggered by some user action in the view (such as clicking a button, selecting a menu item), but:
What should the command do?
Should it use Routing even though WinForms is not mentioned in the documentation? If yes, how is Routing done in a WinForms application?
How would the command/routing request the new form gets shown modally?
For simple messages and yes/no answers, I would look at Wayne Maurer's example for using UserError. I've used his example in Winform projects.
For something more complex, I was having the same difficulties finding any Winforms examples for routing. My google searches finally landed me in the source code for ReactiveUI.Winforms, where I discovered that Paul already has a UserControl for Winforms that will host routed UserControl views. It's called RoutedControlHost.
Using that code, I hacked something together that will show modal forms. I'm sure this isn't the best way to do it, but it might give you ideas.
RoutedModalHost
using Microsoft.Win32.SafeHandles;
using ReactiveUI;
using System;
using System.ComponentModel;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace ReactiveUI_Test_Routing
{
public class RoutedModalHost : ReactiveObject, IDisposable
{
readonly CompositeDisposable disposables = new CompositeDisposable();
RoutingState _Router;
IObservable<string> viewContractObservable;
public RoutedModalHost()
{
this.ViewContractObservable = Observable.Return(default(string));
var vmAndContract =
this.WhenAnyObservable(x => x.Router.CurrentViewModel)
.CombineLatest(this.WhenAnyObservable(x => x.ViewContractObservable),
(vm, contract) => new { ViewModel = vm, Contract = contract });
Form viewLastAdded = null;
this.disposables.Add(vmAndContract.Subscribe(x => {
if (viewLastAdded != null)
{
viewLastAdded.Dispose();
}
if (x.ViewModel == null)
{
return;
}
IViewLocator viewLocator = this.ViewLocator ?? ReactiveUI.ViewLocator.Current;
IViewFor view = viewLocator.ResolveView(x.ViewModel, x.Contract);
view.ViewModel = x.ViewModel;
viewLastAdded = (Form)view;
viewLastAdded.ShowDialog();
}, RxApp.DefaultExceptionHandler.OnNext));
}
[Category("ReactiveUI")]
[Description("The router.")]
public RoutingState Router
{
get { return this._Router; }
set { this.RaiseAndSetIfChanged(ref this._Router, value); }
}
[Browsable(false)]
public IObservable<string> ViewContractObservable
{
get { return this.viewContractObservable; }
set { this.RaiseAndSetIfChanged(ref this.viewContractObservable, value); }
}
[Browsable(false)]
public IViewLocator ViewLocator { get; set; }
bool disposed = false;
SafeHandle handle = new SafeFileHandle(IntPtr.Zero, true);
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
return;
if (disposing)
{
handle.Dispose();
// Free any other managed objects here.
//
this.disposables.Dispose();
}
// Free any unmanaged objects here.
//
disposed = true;
}
}
}
MainViewModel
using ReactiveUI;
using System.Reactive.Linq;
using System;
namespace ReactiveUI_Test_Routing
{
public class MainViewModel : ReactiveObject, IScreen
{
public RoutingState Router { get; private set; }
public ReactiveCommand<object> ShowTestModalForm { get; protected set; }
public MainViewModel(RoutingState modalRouter)
{
Router = modalRouter;
ShowTestModalForm = ReactiveCommand.Create();
ShowTestModalForm.Subscribe(x => Router.Navigate.Execute(new TestModalFormViewModel(this)));
}
}
}
MainView
using System.Windows.Forms;
using Splat;
using ReactiveUI;
namespace ReactiveUI_Test_Routing
{
public partial class MainView : Form, IViewFor<MainViewModel>
{
public MainView()
{
InitializeComponent();
IMutableDependencyResolver dependencyResolver = Locator.CurrentMutable;
dependencyResolver.Register(() => new TestModalFormView(), typeof(IViewFor<TestModalFormViewModel>));
RoutingState router = new RoutingState();
RoutedModalHost modalHost = new RoutedModalHost();
modalHost.Router = router;
this.BindCommand(ViewModel, vm => vm.ShowTestModalForm, v => v.ShowTestModalForm);
ViewModel = new MainViewModel(router);
}
public MainViewModel ViewModel { get; set; }
object IViewFor.ViewModel
{
get { return ViewModel; }
set { ViewModel = (MainViewModel)value; }
}
}
}
TestModalFormViewModel
using ReactiveUI;
namespace ReactiveUI_Test_Routing
{
public class TestModalFormViewModel : ReactiveObject, IRoutableViewModel
{
public IScreen HostScreen { get; protected set; }
public string UrlPathSegment { get { return "ModalForm"; } }
public TestModalFormViewModel(IScreen screen)
{
HostScreen = screen;
}
}
}

How to exit from wpf app

I have a WPF project that is part of the solution, it is "ProjectFilesSelector". A some project, named A. call ProjectFilesSelector as figure below:
!!! UPDATED:
namespace ProjectFilesSelector
{
...
public class ViewModel
{
...
public ICommand cancel
{
get
{
return new WPFExtensions.RelayCommand(_ =>
{
this.window.Visibility = Visibility.Hidden;
this.window.Close();
});
}
}
}
public partial class Window1 : Window, IDisposable
{
public Window1(ProjectTypes.Project pro)
{
InitializeComponent();
var context = new ViewModel(this, new ATChecker.ViewModel.ProjectModel(pro));
this.DataContext = context;
}
...
}
}
namespace ATCheckerView
{
public class ViewerClientExt : INotifyPropertyChanged
{
...
public ICommand CheckPrinciplies
{
get
{
var cmnd =
new RelayCommand(project =>
{
var proj = (ViewModel.ProjectModel)project;
ProjectFilesSelector.ViewModel dc;
using (var a = new ProjectFilesSelector.Window1(proj.project))
{
a.ShowDialog(); // cancel command was called
dc = (ProjectFilesSelector.ViewModel)a.DataContext;
}
....
// some code
// and I can still see the window of Window1. Why?
});
I don t think you want to exit the application. You may just want to close the window.
The cancel button just set IsCancel to true. And for the ok button the best way is to create a event in the viewmodel to get up to the view.

MVVM - Filling a combobox and how to relay commands from window to user control

I have an ItemType that is coming from EF. This ItemType is wrapped in a ItemTypeViewModel. Many ItemTypes are wrapped in ItemTypeViewModels and are being put in a ObservableCollection in the ViewModel for the user control that will display them:
I use the CollectionView so I can page through them. The screen looks like this:
Now I'm thinking that the buttons that are used for paging that are in the user control could better be placed in the Window that will contain the user control. So, in my user control I know have commands like this:
But I want them to be in the window. I don't know if this will be good design, but if I will go through with this, how to relay the commands from the window to the usercontrol?
Another question I have is how to fill the combobox in the user control. They will always have the same values, but the selected item will change per ItemType.
I know two ways how to do this.
1) Add new class, for example, MainWindowViewModel and add there 2 commands and an instance of UserControlViewModel (you haven't said the title, so I will call it in this way). Here is a part of example:
public class MainWindowViewModel
{
public UserControlViewModel ChildControlViewModel { get; set; }
private Lazy<RelayCommand> nextCommand = new Lazy<RelayCommand>(() =>
new RelayCommand(
() => this.ChildControlViewModel.CollectionView.MoveCurrentToNext(),
() => this.ChildControlViewModel.CollectionView.CurrentPosition < this.ChildControlViewModel.ItemTypes.Count - 1));
public ICommand NextCommand
{
get { return nextCommand.Value; }
}
//prev command...
}
I have used the Lazy class, but the main idea is clear: the code is the same, except the call this.ChildControlViewModel.CollectionView instead of CollectionView.
2) Use the Messenger class.
This way isn't so obvious and it has only one advantage: the viewmodels are loosely connected.
public class MainWindowViewModel
{
public const string NextCommandNotification = "NextCommand";
public const string PreviousCommandNotification = "PreviousCommand";
private bool isNextCommandEnabled;
private bool isPreviousCommandEnabled;
public MainWindowViewModel()
{
this.NextCommand = new RelayCommand(
() => Messenger.Default.Send(new NotificationMessage<MainWindowViewModel>(this, NextCommandNotification)),
() => this.isNextCommandEnabled);
//prev command...
Messenger.Default.Register<NotificationMessage<UserControlViewModel>>(this,
msg =>
{
if (msg.Notification == UserControlViewModel.CurrentItemChangedNotification)
{
this.isNextCommandEnabled = msg.Content.CollectionView.CurrentPosition < msg.Content.ItemTypes.Count - 1;
this.NextCommand.RaiseCanExecuteChanged();
//prev command...
}
});
}
public ICommand NextCommand { get; private set; }
//prev command...
}
public class UserControlViewModel
{
public const string CurrentItemChangedNotification = "CurrentItemChanged";
public UserControlViewModel()
{
Messenger.Default.Register<NotificationMessage<MainWindowViewModel>>(this,
msg =>
{
if (msg.Notification == MainWindowViewModel.NextCommandNotification)
this.CollectionView.MoveCurrentToNext();
else if (msg.Notification == MainWindowViewModel.PreviousCommandNotification)
this.CollectionView.MoveCurrentToPrevious();
});
this.CollectionView.CurrentChanged += (s,e) => Messenger.Default.Send(new NotificationMessage<UserControlViewModel>(this, CurrentItemChangedNotification))
}
}
I'm not sure whether this code will work correctly. And it is not easy to explain.
The MainWindowViewModel class send the message when a user press the button. The UserControlViewModel class process the message, change the position of the current item, and send the CurrentItemChangedNotification message. The MainWindowViewModel class process this message and updates the CanExecute part of the command.
1st solution is better for me, but at the same time I use the Messenger class quite often. It depends on the situation.

Open new Window from view model

Hi I have a beginner problem. I have shell (it is wpf window) and in this shell is screen (it is an user control / view model).
I would like open new window from view model, not show user control in shell.
So I create new window - ChatView
<Window x:Class="Spirit.Views.ChatView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:extToolkit="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit.Extended" Title="ChatView" Height="545" Width="763">
<Grid Margin="4,4,4,4">
</Grid>
</Window>
Export ChatViewModel with MEF.
public interface IChatViewModel
{
}
[Export("ChatScreen",typeof(IChatViewModel))]
public class ChatViewModel
{
}
In view model I have this method:
With ShowScreen class help me Mr.Marco Amendola. It look likes this:
public class ShowScreen : IResult
{
readonly Type _screenType;
readonly string _name;
[Import]
public IShellViewModel Shell { get; set; }
Action<object> _initializationAction = screen => { };
public ShowScreen InitializeWith<T>(T argument)
{
_initializationAction = screen =>
{
var initializable = screen as IInitializable<T>;
if (initializable != null)
initializable.Initialize(argument);
};
return this;
}
public ShowScreen(string name)
{
_name = name;
}
public ShowScreen(Type screenType)
{
_screenType = screenType;
}
public void Execute(ActionExecutionContext context)
{
var screen = !string.IsNullOrEmpty(_name)
? IoC.Get<object>(_name)
: IoC.GetInstance(_screenType, null);
_initializationAction(screen);
Shell.ActivateItem(screen);
Completed(this, new ResultCompletionEventArgs());
}
public event EventHandler<ResultCompletionEventArgs> Completed = delegate { };
public static ShowScreen Of<T>()
{
return new ShowScreen(typeof(T));
}
}
My problem is if I try show new window it doesn’t works, it works only if I show new user control in shell(window).
I would like achieve behavior something like in skype. You have a main window with listbox, you double clicked on item and it show new chat window.
Main window can publish with EventAggregator on chat window and also chat window can publish on main window. This is my goal.
I know that I can not use class ShowScreen on showing new Window. I would like to know what is correct way to create new window from view model and inject event aggregator
to this vie model.
Any advice? Thank for your help and time.
Have you looked at WindowManager.Show or WindowManager.ShowDialog? Rob has a sample at http://caliburnmicro.codeplex.com/wikipage?title=The%20Window%20Manager. You can inject this dependency into your view model as IWindowManager.
I'm using this. Maybe could save a question about "where's the code ?".
DialogHelper:
public class DialogHelper
{
public void ShowDialog<T>(params Object[] param) where T : class
{
var windowManager = new WindowManager();
T viewModel = Activator.CreateInstance(typeof(T), param) as T;
windowManager.ShowWindow(viewModel);
}
}
How to use:
Without constructor parameter:
Dialog.ShowDialog<TestTableViewModel>();
With constructor paramater:
Dialog.ShowDialog<TestTableViewModel>(dt);
Note that I'm not using MEF

Resources