-1

I'm trying to wrap my head around ReactiveUI. Most of it makes some degree of sense if you don't look at it too closely, but when I try to set up a TabControl everything explodes in my face.

I have a TabControl on my Window. I want to be able to add multiple different types of tabs to it dynamically at runtime based on different user actions. This answer explains a standard WPF way to do that, and it almost works, but since it's not ReactiveUI, anytime I try to open a tab with a Reactive view on it, everything blows up because the ViewModel dependency property hasn't been bound.

XAML:

    <Window.Resources>
        <DataTemplate DataType="{x:Type vm:MyTabViewModel}">
            <views:MyTabEditor/>
        </DataTemplate>
    </Window.Resources>
...
        <TabControl Name="Multitab" Grid.Column="2" ItemsSource="{Binding Tabs}">
            <TabControl.ItemContainerStyle>
                <Style TargetType="{x:Type TabItem}">
                    <Setter Property="Header" Value="{Binding Name}" />
                </Style>
            </TabControl.ItemContainerStyle>
        </TabControl>

ViewModel:

        public ObservableCollection<ITabPage> Tabs { get; } = new();

        public void AddNewTab()
        {
            var vm = new MyTabEditorViewModel();
            Tabs.Add(vm);
        }

XAML.cs

        private void NewTab_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            ViewModel.AddNewJob();
            Multitab.SelectedIndex = Multitab.Items.Count - 1;
            var last = Multitab.SelectedIndex;
            this.OneWayBind(ViewModel, vm => vm.Tabs[last], v => GAH WHAT GOES HERE?!?);
        }

And this is the part where I get lost. How do I set up the bindings to bind the new VM to the view that gets created for it? Multitab.SelectedItem returns the VM, not the View, and I can't seem to find any way to obtain the newly-created View object in order to bind it.

Does anyone know how to set this up properly?

Mason Wheeler
  • 82,511
  • 50
  • 270
  • 477
  • `MyTabEditor` will be resolved by the framework based on the `MyTabViewModel` that you add to the `ObservableCollection`. You can then bind as usual, for example using `this.OneWayBind`. What are you trying to do in `NewTab_Executed`? – mm8 Aug 11 '21 at 14:16
  • @mm8 How can I "bind as usual ... using `this.OneWayBind`" when I don't have a reference to the new view I'm trying to bind to? Yes, it gets resolved by the framework, but as near as I can tell that's all living in UI land somewhere and I can't see the result of the resolution. – Mason Wheeler Aug 11 '21 at 15:28
  • `this.OneWayBind` goes in `MyTabEditor`. – mm8 Aug 11 '21 at 18:31
  • @mm8 That just moves the problem around. Now I don't have a reference to the ViewModel for the binding. – Mason Wheeler Aug 11 '21 at 18:36
  • I still don't understand what `NewTab_Executed` is supposed to do. What are you trying to bind to? – mm8 Aug 11 '21 at 18:37
  • I'm trying to bind the new ViewModel to the new View that the framework automagically creates for it. Unfortunately, I can't find any point where I have a reference to both. – Mason Wheeler Aug 11 '21 at 18:38
  • The framework resolved the view from the view model. The data context of the view is the view model. – mm8 Aug 11 '21 at 18:39
  • So if the `OneWayBind` code is supposed to go in the newly-created View, why is it in the *parent* View in all of the example code I'm looking at? What am I missing here? – Mason Wheeler Aug 11 '21 at 18:41
  • What example? Maybe you should consider improving your own one. – mm8 Aug 11 '21 at 18:42
  • @mm8 I'm working with [NodeNetwork](https://github.com/Wouterdek/NodeNetwork), and the pattern of "`OneWayBind` calls go in the parent view's codebehind" is pretty consistent in the various code examples it ships with. – Mason Wheeler Aug 11 '21 at 18:50
  • Are you asking SO how some NodeNetwork samples are implented and why...? Or what is your real question? – mm8 Aug 11 '21 at 18:52
  • Why so hostile? It was *your* question, what my example code was. I answered. There isn't all that much in the way of good documentation on this stuff, so the sample code I have for the library I'm working with is what I have to go on. I'm trying to learn this stuff here. – Mason Wheeler Aug 11 '21 at 18:58
  • Sorry, I wasn't mean to be hostile. I just don't understand your question. – mm8 Aug 11 '21 at 18:58
  • The question is exactly what I asked. If you're claiming that binding should happen in the new View, what's the rationale for putting it in the parent View instead? Even when people do the wrong thing, *they do it for a reason that seems good to them,* and I don't have the context to be able to grasp or even guess at what that reason is. – Mason Wheeler Aug 11 '21 at 19:02
  • What binding? What target property are you trying to bind to what source property? – mm8 Aug 11 '21 at 19:05
  • I'm trying to bind the newly-created VM to the `ViewModel` property on the new view. – Mason Wheeler Aug 11 '21 at 19:06
  • I put together an example for you. Hope it helps. – mm8 Aug 11 '21 at 19:36

1 Answers1

3

Please refer to the below sample code.

Window1.xaml:

<reactiveui:ReactiveWindow x:Class="WpfApp1.Window1"
        x:TypeArguments="local:ViewModel"
        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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        xmlns:reactiveui="http://reactiveui.net"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">
    <Grid>
        <TabControl Name="Multitab">
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}" />
                </DataTemplate>
            </TabControl.ItemTemplate>
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <reactiveui:ViewModelViewHost ViewModel="{Binding}" />
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
    </Grid>
</reactiveui:ReactiveWindow>

Window.xaml.cs:

public partial class Window1 : ReactiveWindow<ViewModel>
{
    public Window1()
    {
        InitializeComponent();
        ViewModel = new ViewModel();

        this.WhenActivated(disposableRegistration =>
        {
            this.OneWayBind(ViewModel,
                viewModel => viewModel.Tabs,
                view => view.Multitab.ItemsSource)
                .DisposeWith(disposableRegistration);
        });
    }
}

View Model:

public class ViewModel
{
    public ObservableCollection<ITabPage> Tabs { get; } = 
        new ObservableCollection<ITabPage>() { new MyTabEditorViewModel() };
}

Tab View Model:

public interface ITabPage { }

public class MyTabEditorViewModel : ITabPage
{
    public string Name { get; } = "Name...";
}

TabView.xaml:

<reactiveui:ReactiveUserControl x:Class="WpfApp1.TabEditorView"
             x:TypeArguments="local:MyTabEditorViewModel"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:reactiveui="http://reactiveui.net"
             xmlns:local="clr-namespace:WpfApp1"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <Grid>
        <TextBlock>Tab content...</TextBlock>
    </Grid>
</reactiveui:ReactiveUserControl>

TabView.xaml.cs:

public partial class TabEditorView : ReactiveUserControl<MyTabEditorViewModel>
{
    public TabEditorView()
    {
        InitializeComponent();
    }
}

enter image description here

mm8
  • 163,881
  • 10
  • 57
  • 88