diff --git a/src/ReactiveUI.Tests/Mocks/MockBindListItemViewModel.cs b/src/ReactiveUI.Tests/Mocks/MockBindListItemViewModel.cs new file mode 100644 index 0000000000..e44f2378ef --- /dev/null +++ b/src/ReactiveUI.Tests/Mocks/MockBindListItemViewModel.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +namespace ReactiveUI.Tests +{ + public class MockBindListItemViewModel : ReactiveObject + { + private string _name = string.Empty; + + public MockBindListItemViewModel(string name) => Name = name; + + /// + /// Gets or sets displayed name of the crumb. + /// + public string Name + { + get => _name; + set => this.RaiseAndSetIfChanged(ref _name, value); + } + } +} diff --git a/src/ReactiveUI.Tests/Mocks/MockBindListView.cs b/src/ReactiveUI.Tests/Mocks/MockBindListView.cs new file mode 100644 index 0000000000..70cda838ff --- /dev/null +++ b/src/ReactiveUI.Tests/Mocks/MockBindListView.cs @@ -0,0 +1,80 @@ +// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System.IO; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Markup; + +namespace ReactiveUI.Tests +{ + /// + /// MockBindListView. + /// + /// + public class MockBindListView : UserControl, IViewFor + { + public static readonly DependencyProperty ViewModelProperty = + DependencyProperty.Register(nameof(ViewModel), typeof(MockBindListViewModel), typeof(MockBindListView), new PropertyMetadata(null)); + + /// + /// Initializes a new instance of the class. + /// + public MockBindListView() + { + ItemList = new(); + + var ms = new MemoryStream(Encoding.UTF8.GetBytes(@" + + + + + ")); + ItemList.ItemTemplate = (DataTemplate)XamlReader.Load(ms); + var ms1 = new MemoryStream(Encoding.UTF8.GetBytes(@" + + + ")); + ItemList.ItemsPanel = (ItemsPanelTemplate)XamlReader.Load(ms1); + + ViewModel = new(); + this.WhenActivated(d => + { + this.OneWayBind(ViewModel, vm => vm.ListItems, v => v.ItemList.ItemsSource).DisposeWith(d); + this.WhenAnyValue(v => v.ItemList.SelectedItem) + .Where(i => i != null) + .Cast() + .Do(_ => ItemList.UnselectAll()) + .InvokeCommand(this, v => v.ViewModel!.SelectItem).DisposeWith(d); + }); + } + + /// + /// Gets or sets the ViewModel corresponding to this specific View. This should be + /// a DependencyProperty if you're using XAML. + /// + public MockBindListViewModel? ViewModel + { + get => (MockBindListViewModel)GetValue(ViewModelProperty); + set => SetValue(ViewModelProperty, value); + } + + public ListView ItemList { get; } + + object? IViewFor.ViewModel + { + get => ViewModel; + set => ViewModel = (MockBindListViewModel?)value; + } + } +} diff --git a/src/ReactiveUI.Tests/Mocks/MockBindListViewModel.cs b/src/ReactiveUI.Tests/Mocks/MockBindListViewModel.cs new file mode 100644 index 0000000000..15e0b36ec4 --- /dev/null +++ b/src/ReactiveUI.Tests/Mocks/MockBindListViewModel.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved. +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for full license information. + +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using DynamicData; + +namespace ReactiveUI.Tests +{ + public class MockBindListViewModel : ReactiveObject + { + private readonly ObservableAsPropertyHelper _activeItem; + private readonly ReadOnlyObservableCollection _listItems; + + static MockBindListViewModel() + { + Splat.Locator.CurrentMutable.Register(() => new MockBindListView(), typeof(IViewFor)); + } + + /// + /// Initializes a new instance of the class. + /// + public MockBindListViewModel() + { + SelectItem = ReactiveCommand.Create((MockBindListItemViewModel item) => + { + ActiveListItem.Edit(l => + { + var index = l.IndexOf(item); + for (var i = l.Count - 1; i > index; i--) + { + l.RemoveAt(i); + } + }); + }); + + ActiveListItem.Connect() + .Select(_ => ActiveListItem.Count > 0 ? ActiveListItem.Items.ElementAt(ActiveListItem.Count - 1) : null) + .ToProperty(this, vm => vm.ActiveItem, out _activeItem); + + ActiveListItem.Connect().ObserveOn(RxApp.MainThreadScheduler).Bind(out _listItems).Subscribe(); + } + + /// + /// Gets the item that is currently loaded in the list. + /// Add or remove elements to modify the list. + /// + public ISourceList ActiveListItem { get; } = new SourceList(); + + /// + /// Gets the deepest item of the currect list. (Last element of ActiveListItem). + /// + public MockBindListItemViewModel? ActiveItem => _activeItem.Value; + + /// + /// Gets the items to be represented by the selected item which is passed as a parameter. + /// Only this item and its ancestors are kept, the rest of the items are removed. + /// + public ReactiveCommand SelectItem { get; } + + /// + /// Gets the list items. + /// + /// + /// The list items. + /// + public ReadOnlyObservableCollection ListItems => _listItems; + } +} diff --git a/src/ReactiveUI.Tests/Platforms/windows-xaml/PropertyBindingTest.cs b/src/ReactiveUI.Tests/Platforms/windows-xaml/PropertyBindingTest.cs index 1543d4a25b..54b34a75e7 100644 --- a/src/ReactiveUI.Tests/Platforms/windows-xaml/PropertyBindingTest.cs +++ b/src/ReactiveUI.Tests/Platforms/windows-xaml/PropertyBindingTest.cs @@ -8,10 +8,14 @@ using System.Globalization; using System.Linq; using System.Reactive; +using System.Reactive.Concurrency; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reactive.Subjects; +using System.Windows; +using DynamicData; using DynamicData.Binding; +using ReactiveUI.Tests.Wpf; using Xunit; #if NETFX_CORE @@ -689,5 +693,41 @@ public void BindWithFuncToTriggerUpdateTest() dis.Dispose(); Assert.True(dis.IsDisposed); } + + [StaFact] + public void BindListFunctionalTest() + { + var window = new WpfTestWindow(); + var view = new MockBindListView(); + window.RootGrid.Children.Add(view); + + var loaded = new RoutedEventArgs + { + RoutedEvent = FrameworkElement.LoadedEvent + }; + + window.RaiseEvent(loaded); + view.RaiseEvent(loaded); + var test1 = new MockBindListItemViewModel("Test1"); + view.ViewModel?.ActiveListItem.Add(test1); + Assert.Equal(1, view.ItemList.Items.Count); + Assert.Equal(test1, view.ViewModel!.ActiveItem); + + var test2 = new MockBindListItemViewModel("Test2"); + view.ViewModel?.ActiveListItem.Add(test2); + Assert.Equal(2, view.ItemList.Items.Count); + Assert.Equal(test2, view.ViewModel!.ActiveItem); + + var test3 = new MockBindListItemViewModel("Test3"); + view.ViewModel?.ActiveListItem.Add(test3); + Assert.Equal(3, view.ItemList.Items.Count); + Assert.Equal(test3, view.ViewModel!.ActiveItem); + + view.ItemList.SelectedItem = view.ItemList.Items.GetItemAt(0); + Assert.Equal(1, view.ItemList.Items.Count); + Assert.Equal(test1, view.ViewModel!.ActiveItem); + + window.Dispatcher.InvokeShutdown(); + } } } diff --git a/src/ReactiveUI.Tests/ReactiveUI.Tests.csproj b/src/ReactiveUI.Tests/ReactiveUI.Tests.csproj index f70c13f4a3..5d5db8634d 100644 --- a/src/ReactiveUI.Tests/ReactiveUI.Tests.csproj +++ b/src/ReactiveUI.Tests/ReactiveUI.Tests.csproj @@ -1,4 +1,4 @@ - + netcoreapp3.1 @@ -60,19 +60,15 @@ + + true + true + - - - - - - - - @@ -81,6 +77,10 @@ + + + + True diff --git a/src/ReactiveUI.sln b/src/ReactiveUI.sln index 0762ed0ad2..4175d99f02 100644 --- a/src/ReactiveUI.sln +++ b/src/ReactiveUI.sln @@ -59,6 +59,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.Drawing", "React EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUI.XamForms.Tests", "ReactiveUI.XamForms.Tests\ReactiveUI.XamForms.Tests.csproj", "{46D5C71E-2E58-4454-BE3A-30B9047A2D1E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Src", "Src", "{EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{2AE709FA-BE58-4287-BFD3-E80BEB605125}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -153,6 +157,29 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {464CB812-F99F-401B-BE4C-E8F0515CD19D} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {F5A6E11B-B074-4A1C-B937-267D840E31DF} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {DDF89A7A-5CC9-4243-98E4-462860D5D963} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {C11F6165-6142-476F-83F1-CFEBC330C769} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {84A9E530-93D7-4108-9887-690127F70AF5} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {B311B0EC-CEF3-45E6-BA7A-EC6AB58E7E7D} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {2ADE0A50-5012-4341-8F4F-97597C2D6920} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} + {15CF3AA6-9F7C-4F23-BAE7-4A93352E94B6} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {1AC71A71-F5F3-4F96-BDA9-A9DC7F572DB9} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} + {7DE43BB9-5AC8-446A-8D8B-88C9201D802E} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {20750BB4-36DD-4F8C-B970-D7809810EC98} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {404B0F3F-7343-4E54-A863-F27B99FE788B} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} + {7ED6D69F-138F-40BD-9F37-3E4050E4D19B} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} + {CD8B19A9-316E-4FBC-8F0C-87ADC6AAD684} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} + {36FC3269-B7D0-4D79-A54A-B26B6190E8A2} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {0C7EDFF0-80BE-4FFC-A1B9-0C48043B71F3} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {86430CEC-BAA3-4ED4-A90D-982437137D19} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {366789D4-4117-46CE-864F-BF1201F35319} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {1FD70B38-E2FB-4A46-BE07-C0F6ACE6FD90} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} + {999D555D-C567-457C-95F7-8AD61310C56E} = {EF7ED1B0-00E4-4CD0-9741-0D1D4463B8EC} + {46D5C71E-2E58-4454-BE3A-30B9047A2D1E} = {2AE709FA-BE58-4287-BFD3-E80BEB605125} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9326B58C-0AD3-4527-B3F4-86B54673C62E} EndGlobalSection