Q: How can I bind a ViewModel to a ReactiveUserControl? Or how to nest Reactiveui views?
There's probably something I'm doing wrong, but I can't figure out what exactly.
ReactiveUserControl
// MenuView.xaml
<reactiveui:ReactiveUserControl
x:Class="Views.MenuView"
xmlns:menuItems="clr-namespace:Model"
.... >
<Menu x:Name="RootMenu"
IsMainMenu="True">
<Menu.Resources>
<DataTemplate DataType="{x:Type menuItems:DialogItem}">
<TextBlock Text="{Binding Description}" />
</DataTemplate>
</Menu.Resources>
</Menu>
</reactiveui:ReactiveUserControl>
// MenuView.xaml.cs
namespace Views
{
public partial class MenuView : ReactiveUserControl<MenuViewModel>
{
public MenuView()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
this.OneWayBind(ViewModel,
vm => vm.MenuItems,
view => view.RootMenu.ItemsSource
).DisposeWith(disposables);
});
}
}
}
// MenuViewModel.cs
namespace Views
{
public class MenuViewModel : ReactiveObject
{
public ObservableCollection<DialogItem> MenuItems { get; } = new ObservableCollection<DialogItem>();
public MenuViewModel()
{
MenuItems.Add(new DialogItem("Edit", 224));
MenuItems.Add(new DialogItem("View", 224));
}
}
}
DialogItem represents an item in the menu
// DialogItem.cs
namespace Model
{
public class DialogItem
{
public DialogItem(string description, int dialogId)
{
this.DialogId = dialogId;
this.Description = description;
}
public int DialogId { get; }
public string Description { get; }
}
}
Then finally in MainWindow I include the usercontrol like so:
// MainWindow.xaml
<reactiveui:ReactiveWindow
x:Class="Views.MainWindow"
....
>
<Grid>
<views:MenuView x:Name="MainMenu" />
</Grid>
</reactiveui:ReactiveWindow>
Code behind
// MainWindow.xaml.cs
namespace Views
{
public partial class MainWindow : ReactiveWindow<MainWindowModel>
{
public MainWindow()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
// BIND THE VIEWMODEL CREATED IN THE MAINWINDOW VIEWMODEL, IS THIS CORRECT?
this.Bind(ViewModel,
vm => vm.MainMenuViewModel,
view => view.MainMenu.ViewModel
).DisposeWith(disposables);
});
}
}
}
// MainWindowModel.cs
namespace Views
{
public class MainWindowModel : ReactiveObject
{
public MenuViewModel MainMenuViewModel { get; }
public MainWindowModel()
{
this.MainMenuViewModel = new MenuViewModel();
}
}
}
The Items are iterated, but if I look at the visual representation tree I an ViewModelViewHost item in the ContentPresenter instead of a TextBlock
Here you see the Menu Items are not rendered correctly. (they are rendered but without the text from DataTemplate.
Update:
Using a Menu.ItemTemplate does work, but this is not what I am looking for.
<reactiveui:ReactiveUserControl
x:Class="Views.MenuView"
...
>
<Menu x:Name="RootMenu"
IsMainMenu="True">
<Menu.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Description}" />
</DataTemplate>
</Menu.ItemTemplate>
</Menu>
</reactiveui:ReactiveUserControl>
Discussing this issue on Slack with Glenn Watson gave me the penny drop moment. Because I was using the code behind binding of RxUI, RxUI is using its locator logic to lookup the view. RxUI will lookup a view if there's no ItemTemplate or DisplayPathMember property defined. These views are registered in the Splat container as a view for the viewmodel. So the DataTemplates in <Menu.Resources> are not considered. This is exactly what's shown in the image. A ViewModelViewHost is created, but because no corresponding view is found nothing is displayed.
This can be solved by using the XAML binding instead of the code behind binding (assign the viewmodel to the datacontext to do this!). See the docs for more info.
Related
I have one View that has another one inside. I want to make ViewModel for both of them. But apparently Binding in Child View is not working properly or I have done wrong binding, perhaps.
I have debugged that Child ViewModel is recreated every time I have selected different row in Parent ViewModel.
But UI, doesn't refresh, despite UpdateSourceTrigger=PropertyChanged.
If I edit Binding in XAML while running app then it gets refreshed (as Binding probably is regenerated).
I could set UpdateSourceTrigger=Explicit, but I can't call UpdateSource from none of ViewModels.
PARENT VIEW:
<UserControl ... DataContext="{Binding ProjectsViewModel, Source={StaticResource ViewModelLocator}}">
<Grid>
<poc:AdvancedListView ItemsSource="{Binding Projects}" SelectedObject="{Binding SelectedProject, Mode=TwoWay}"/>
...
<ScrollViewer>
<StackPanel Orientation="Vertical">
...
<poc:Section SectionName="ATTACHMENTS">
<poc:AttachmentsControl DataContext="{Binding AttachmentsViewModel, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" /> // THIS IS BINDING BETWEEN VM
</poc:Section>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>
PARENT VIEWMODEL:
public class ProjectsViewModel : BaseViewModel
{
public ProjectsViewModel(ObservableCollection<Project> projects)
{
this.Projects = projects;
}
public ObservableCollection<Project> Projects { get; }
private Project selectedProject;
public Project SelectedProject
{
get { return selectedProject; }
set
{
SetPropertyAndNotify(ref selectedProject, value);
AttachmentsViewModel = new AttachmentsViewModel(selectedProject.Attachments); // THIS IS CREATION OF CHILD VM
}
}
public AttachmentsViewModel AttachmentsViewModel { get; set; }
}
CHILD VIEW:
<UserControl ... x:Name="attachmentControl">
<Grid x:Name="mainGrid">
...
<ListView x:Name="attachmentsListView" ItemsSource="{Binding Attachments, Mode=OneWay, UpdateSourceTrigger=PropertyChanged}" SelectionMode="Single"> // THIS IS BINDING TO LIST THAT IS NOT WORKING
<ListView.View>
<GridView>
...
</GridView>
</ListView.View>
</ListView>
</Grid>
</UserControl>
CHILD VIEWMODEL:
public class AttachmentsViewModel : BaseViewModel
{
public ObservableCollection<Attachment> Attachments { get; set; }
public AttachmentsViewModel(ObservableCollection<Attachment> attachments)
{
Attachments = attachments;
}
}
What I do wrong or what concept I have understood wrong?
public class ProjectsViewModel : BaseViewModel
{
public ProjectsViewModel(ObservableCollection<Project> projects)
{
this.Projects = projects;
}
public ObservableCollection<Project> Projects { get; }
private Project selectedProject;
public Project SelectedProject
{
get { return selectedProject; }
set
{
SetPropertyAndNotify(ref selectedProject, value);
// THIS IS CREATION OF CHILD VM
AttachmentsViewModel = new AttachmentsViewModel(selectedProject.Attachments);
}
}
private AttachmentsViewModel _attachmentsViewModel;
public AttachmentsViewModel AttachmentsViewModel
{
get => _attachmentsViewModel;
set => SetPropertyAndNotify(_attachmentsViewModel, value);
}
}
public class AttachmentsViewModel : BaseViewModel
{
// This should be a Read Only property
public ObservableCollection<Attachment> Attachments { get; /* set; */}
public AttachmentsViewModel(ObservableCollection<Attachment> attachments)
{
Attachments = attachments;
}
}
Additional recommendation: Adding extra logic to the property setter - is bad.
Surely in the BaseViewModel implementation there is an opportunity to set the dependence of properties on each other in a different way.
I'm working on a WPF MVVM application. I'm looking to databind a WebBrowser control to a view model which is in turn bound to a Tab. Following the advice in this article, I created a static helper class consisting of a static DependancyProperty:
public static class WebBrowserHelper
{
public static readonly DependencyProperty BodyProperty =
DependencyProperty.RegisterAttached("Body", typeof(string), typeof(WebBrowserHelper), new PropertyMetadata(OnBodyChanged));
public static string GetBody(DependencyObject dependencyObject)
{
return (string)dependencyObject.GetValue(BodyProperty);
}
public static void SetBody(DependencyObject dependencyObject, string body)
{
dependencyObject.SetValue(BodyProperty, body);
}
private static void OnBodyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
string newValue = (string)e.NewValue;
var webBrowser = (WebBrowser)d;
webBrowser.NavigateToString(newValue);
}
}
XAML Binding WebBrowser to DependancyProperty:
<WebBrowser Grid.Column="2" HorizontalAlignment="Center" src:WebBrowserHelper.Body="{Binding HTMLBody}" VerticalAlignment="Center" Height="Auto" Width="Auto" />
ViewModel that bound to ItemsSource of Tab Control:
public class SomeVM : ViewModelBase, INotifyPropertyChanged
{
private string _htmlBody;
private SomeView _myView = new SomeView();
public SomeVM (string tabName)
{
TabName = tabName;
string contentsAsHTML = do_a_whole_bunch_of_stuff_to_generate_an_HTML_string();
HTMLBody = contentsAsHTML;
}
public string HTMLBody
{
get { return _htmlBody; }
set
{
if (_htmlBody != value)
{
_htmlBody = value;
RaisePropertyChanged("HTMLBody");
}
}
}
public SomeView View
{
get {return _myView;}
set { }
}
public string TabName { get; set; }
}
MainViewModel, Creating the Tab collection:
private ObservableCollection<SomeVM> _tabs;
public ObservableCollection<SomeVM> Tabs
{
get
{
if (_tabs== null)
{
_tabs= new ObservableCollection<SomeVM>();
_tabs.Add(new SomeVM("Tab 1"));
_tabs.Add(new SomeVM("Tab 2"));
_tabs.Add(new SomeVM("Tab 3"));
}
return _tabs;
}
}
MainWindow.xaml setting up the Tab Binding:
<TabControl ItemsSource="{Binding Tabs, Source={StaticResource vm}}"
>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding TabName}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding View}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
My problem is that "OnBodyChanged" is fired multiple times on ever tab change. The HTML takes a few seconds to load, and I would rather it only loads when the property is actually modified in the viewmodel.
EDIT
Here's the smallest sample project that recreates my problem.
Your problem is not relevant to attached properties or MVVM.
In fact, the real problem is that TabControl destroy and recreate its child every time you change the selected tab. That would explain why the handler is invoked more than once. The VisualTree only contains the selected Tab.
If you can try with another control, you will see there are no errors.
For solving this issue, I will redirect you to this post.
I am trying to learn the Prism Navigation support. Presently, I have a prism Region and I want to load view to that region using RegionManager.RequestNavigate(). The navigation does occur, however the IsNavigationTarget() of INavigationAware is not invoked, even if the ViewModel of the Navigation Target view implements INavigationAware interface. Here is the code that I am using.
Shell:
<StackPanel Margin="10">
<TextBlock Text="Main Window"/>
<Button Content="RegionA" Command="{Binding NavigateToACommand}" />
<ContentControl prism:RegionManager.RegionName="MainRegion"/>
</StackPanel>
ShellViewModel:
private void NavigateToA () {
Uri uri = new Uri("RegionAView", UriKind.Relative);
RegionManager.RequestNavigate("MainRegion", uri);
}
RegionAView:
<UserControl x:Class="NavigationExample.RegionAView"
<Grid>
<TextBlock Text="This is Region A"/>
</Grid>
</UserControl>
RegionAViewModel
public class RegionAViewModel : INavigationAware{
public RegionAViewModel() {
}
public bool IsNavigationTarget(NavigationContext navigationContext) {
return false; //Not Invoked
}
public void OnNavigatedTo(NavigationContext navigationContext) {
//Gets Invoked
}
}
RegionAView.xaml.cs
[Export("RegionAView")]
public partial class RegionAView : UserControl {
public RegionAView() {
InitializeComponent();
}
}
Why does the IsNavigationTarget() not getting invoked prior to completion of Navigation?
I think your problem is that you export your view as singleton. modify VM and V as follow:
[Export("RegionAView")]
[PartCreationPolicy(CreationPolicy.NonShared)]
public partial class RegionAView : UserControl
{
public RegionAView()
{
InitializeComponent();
}
}
Basically, IsNavigationTarget will be invoked when you have existing instances. But it will not work for newly created instance.
I have an ObservableCollection of "Layouts" and a "SelectedLocation" DependencyProperty on a Window. The SelectedLocation has a property called "Layout", which is an object containing fields like "Name" etc. I'm trying to bind a combobox to the SelectedLayout but it's not working.
The following does not work, I've tried binding to SelectedItem instead to no avail. I believe it may be something to do with the fact that I'm binding to a subProperty of the SelectedLocation DependencyProperty (though this does implement INotifyPropertyChanged.
<ComboBox Grid.Row="2" Grid.Column="0" x:Name="cboLayout" ItemsSource="{Binding Layouts,ElementName=root}" SelectedValue="{Binding SelectedLocation.Layout.LayoutID,ElementName=root}" DisplayMemberPath="{Binding Name}" SelectedValuePath="LayoutID" />
However, the following works (Also bound to the "SelectedLocation" DP:
<TextBox Grid.Row="4" Grid.Column="1" x:Name="txtName" Text="{Binding SelectedLocation.Name,ElementName=root,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" />
What type property Layouts has? I suppose something like this this: IEnumerable<Layout>.
But you bind selected value to Layout.LayoutID. So you got situation, when combo box contains Layout objects, and you try to select it by Int identifier. Of course binding engine can't find any Int there.
I have no idea about details of your code, so one thing I could propose: try to reduce your binding expression: SelectedItem="{Binding SelectedLocation.Layout,ElementName=root}.
If no success, provide more code to help me understand what's going on.
====UPDATE====
As I've said, you are obviously doing something wrong. But I am not paranormalist and couldn't guess the reason of your fail (without your code). If you don't want to share your code, I decided to provide simple example in order to demonstrate that everything works. Have a look at code shown below and tell me what is different in your application.
Class Layout which exposes property LayoutId:
public class Layout
{
public Layout(string id)
{
this.LayoutId = id;
}
public string LayoutId
{
get;
private set;
}
public override string ToString()
{
return string.Format("layout #{0}", this.LayoutId);
}
}
Class SelectionLocation which has nested property Layout:
public class SelectedLocation : INotifyPropertyChanged
{
private Layout _layout;
public Layout Layout
{
get
{
return this._layout;
}
set
{
this._layout = value;
this.OnPropertyChanged("Layout");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
var safeEvent = this.PropertyChanged;
if (safeEvent != null)
{
safeEvent(this, new PropertyChangedEventArgs(name));
}
}
}
And Window class with dependency properties (actually, in my example StartupView is UserControl, but it doesn't matter):
public partial class StartupView : UserControl
{
public StartupView()
{
InitializeComponent();
this.Layouts = new Layout[] { new Layout("AAA"), new Layout("BBB"), new Layout("CCC") };
this.SelectedLocation = new SelectedLocation();
this.SelectedLocation.Layout = this.Layouts.ElementAt(1);
}
public IEnumerable<Layout> Layouts
{
get
{
return (IEnumerable<Layout>)this.GetValue(StartupView.LayoutsProperty);
}
set
{
this.SetValue(StartupView.LayoutsProperty, value);
}
}
public static readonly DependencyProperty LayoutsProperty =
DependencyProperty.Register("Layouts",
typeof(IEnumerable<Layout>),
typeof(StartupView),
new FrameworkPropertyMetadata(null));
public SelectedLocation SelectedLocation
{
get
{
return (SelectedLocation)this.GetValue(StartupView.SelectedLocationProperty);
}
set
{
this.SetValue(StartupView.SelectedLocationProperty, value);
}
}
public static readonly DependencyProperty SelectedLocationProperty =
DependencyProperty.Register("SelectedLocation",
typeof(SelectedLocation),
typeof(StartupView),
new FrameworkPropertyMetadata(null));
}
XAML of StartupView:
<UserControl x:Class="Test.StartupView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:self="clr-namespace:HandyCopy"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Name="Root">
<WrapPanel>
<ComboBox ItemsSource="{Binding Path=Layouts,ElementName=Root}"
SelectedItem="{Binding Path=SelectedLocation.Layout, ElementName=Root}"/>
</WrapPanel>
</UserControl>
Assuming the following view model definition:
public class MyObject {
public string Name { get; set; }
}
public interface IMyViewModel {
ICommand MyCommand { get; }
IList<MyObject> MyList { get; }
}
And a UserControl with the following code behind:
public class MyView : UserControl {
public IMyViewModel Model { get; }
}
If my XAML looked like this:
<UserControl>
<ListBox ItemsSource="{Binding MyList}">
<ListBox.ItemTemplate>
<TextBlock Text="{Binding Name}" />
<Button Content="Execute My Command" cmd:Click.Command="{Binding Path=MyCommand, ?????????}" cmd:Click.CommandParameter="{Binding}" />
</ListBox.ItemTemplate>
</ListBox>
How can I bind my Button to the ICommand property of my code-behind class?
I'm using Prism and SL 3.0 and I need to bind each button in my list box to the same command on my view model.
Before my UserControl had a specific name and I was able to use the ElementName binding, but now my UserControl is used multiple times in the same parent view so I can't use that technique anymore and I can't figure out how to do this in XAML.
If it is my only option I can do it manually in the code-behind, but I'd rather do it declaratively in the XAML, if possible.
You need a DataContextProxy for this to work because you're no longer in the context of the UserControl. You've moved out of that and there is no good way to reach back into that context without something like the DataContextProxy. I've used it for my projects and it works great.