56 changed files with 1447 additions and 244 deletions
@ -1,22 +0,0 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// Specifies that this object supports a simple, transacted notification for batch
|
|||
/// initialization.
|
|||
/// </summary>
|
|||
public interface ISupportInitialize |
|||
{ |
|||
/// <summary>
|
|||
/// Signals the object that initialization is starting.
|
|||
/// </summary>
|
|||
void BeginInit(); |
|||
|
|||
/// <summary>
|
|||
/// Signals the object that initialization is complete.
|
|||
/// </summary>
|
|||
void EndInit(); |
|||
} |
|||
} |
|||
@ -1,53 +0,0 @@ |
|||
using System; |
|||
using Avalonia.Data; |
|||
|
|||
namespace Avalonia.Logging |
|||
{ |
|||
internal static class LoggerExtensions |
|||
{ |
|||
public static void LogIfError( |
|||
this BindingNotification notification, |
|||
object source, |
|||
AvaloniaProperty property) |
|||
{ |
|||
if (notification.ErrorType == BindingErrorType.Error) |
|||
{ |
|||
if (notification.Error is AggregateException aggregate) |
|||
{ |
|||
foreach (var inner in aggregate.InnerExceptions) |
|||
{ |
|||
LogError(source, property, inner); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
LogError(source, property, notification.Error); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static void LogError(object source, AvaloniaProperty property, Exception e) |
|||
{ |
|||
var level = LogEventLevel.Warning; |
|||
|
|||
if (e is BindingChainException b && |
|||
!string.IsNullOrEmpty(b.Expression) && |
|||
string.IsNullOrEmpty(b.ExpressionErrorPoint)) |
|||
{ |
|||
// The error occurred at the root of the binding chain: it's possible that the
|
|||
// DataContext isn't set up yet, so log at Information level instead of Warning
|
|||
// to prevent spewing hundreds of errors.
|
|||
level = LogEventLevel.Information; |
|||
} |
|||
|
|||
Logger.Log( |
|||
level, |
|||
LogArea.Binding, |
|||
source, |
|||
"Error in binding to {Target}.{Property}: {Message}", |
|||
source, |
|||
property, |
|||
e.Message); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,79 @@ |
|||
using System; |
|||
using System.Collections; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Layout |
|||
{ |
|||
internal class LayoutQueue<T> : IReadOnlyCollection<T> |
|||
{ |
|||
private struct Info |
|||
{ |
|||
public bool Active; |
|||
public int Count; |
|||
} |
|||
|
|||
public LayoutQueue(Func<T, bool> shouldEnqueue) |
|||
{ |
|||
_shouldEnqueue = shouldEnqueue; |
|||
} |
|||
|
|||
private Func<T, bool> _shouldEnqueue; |
|||
private Queue<T> _inner = new Queue<T>(); |
|||
private Dictionary<T, Info> _loopQueueInfo = new Dictionary<T, Info>(); |
|||
private int _maxEnqueueCountPerLoop = 1; |
|||
|
|||
public int Count => _inner.Count; |
|||
|
|||
public IEnumerator<T> GetEnumerator() => (_inner as IEnumerable<T>).GetEnumerator(); |
|||
|
|||
IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); |
|||
|
|||
public T Dequeue() |
|||
{ |
|||
var result = _inner.Dequeue(); |
|||
|
|||
if (_loopQueueInfo.TryGetValue(result, out var info)) |
|||
{ |
|||
info.Active = false; |
|||
_loopQueueInfo[result] = info; |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
|
|||
public void Enqueue(T item) |
|||
{ |
|||
_loopQueueInfo.TryGetValue(item, out var info); |
|||
|
|||
if (!info.Active && info.Count < _maxEnqueueCountPerLoop) |
|||
{ |
|||
_inner.Enqueue(item); |
|||
_loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 }; |
|||
} |
|||
} |
|||
|
|||
public void BeginLoop(int maxEnqueueCountPerLoop) |
|||
{ |
|||
_maxEnqueueCountPerLoop = maxEnqueueCountPerLoop; |
|||
} |
|||
|
|||
public void EndLoop() |
|||
{ |
|||
var notfinalized = _loopQueueInfo.Where(v => v.Value.Count == _maxEnqueueCountPerLoop).ToArray(); |
|||
|
|||
_loopQueueInfo.Clear(); |
|||
|
|||
//prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle
|
|||
//one more time as a final attempt
|
|||
foreach (var item in notfinalized) |
|||
{ |
|||
if (_shouldEnqueue(item.Key)) |
|||
{ |
|||
_loopQueueInfo[item.Key] = new Info() { Active = true, Count = item.Value.Count + 1 }; |
|||
_inner.Enqueue(item.Key); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,6 +1,10 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using Avalonia.Metadata; |
|||
|
|||
[assembly: InternalsVisibleTo("Avalonia.Layout.UnitTests")] |
|||
|
|||
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")] |
|||
|
|||
|
|||
@ -0,0 +1,224 @@ |
|||
using System; |
|||
using System.Reactive.Disposables; |
|||
using System.Reactive.Linq; |
|||
using Avalonia.Animation; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Styling; |
|||
using ReactiveUI; |
|||
using Splat; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// This control hosts the View associated with ReactiveUI RoutingState,
|
|||
/// and will display the View and wire up the ViewModel whenever a new
|
|||
/// ViewModel is navigated to. Nested routing is also supported.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// <para>
|
|||
/// ReactiveUI routing consists of an IScreen that contains current
|
|||
/// RoutingState, several IRoutableViewModels, and a platform-specific
|
|||
/// XAML control called RoutedViewHost.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// RoutingState manages the ViewModel navigation stack and allows
|
|||
/// ViewModels to navigate to other ViewModels. IScreen is the root of
|
|||
/// a navigation stack; despite the name, its views don't have to occupy
|
|||
/// the whole screen. RoutedViewHost monitors an instance of RoutingState,
|
|||
/// responding to any changes in the navigation stack by creating and
|
|||
/// embedding the appropriate view.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// Place this control to a view containing your ViewModel that implements
|
|||
/// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property.
|
|||
/// <code>
|
|||
/// <![CDATA[
|
|||
/// <rxui:RoutedViewHost
|
|||
/// HorizontalAlignment="Stretch"
|
|||
/// VerticalAlignment="Stretch"
|
|||
/// Router="{Binding Router}">
|
|||
/// <rxui:RoutedViewHost.DefaultContent>
|
|||
/// <TextBlock Text="Default Content"/>
|
|||
/// </rxui:RoutedViewHost.DefaultContent>
|
|||
/// </rxui:RoutedViewHost>
|
|||
/// ]]>
|
|||
/// </code>
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// See <see href="https://reactiveui.net/docs/handbook/routing/">
|
|||
/// ReactiveUI routing documentation website</see> for more info.
|
|||
/// </para>
|
|||
/// </remarks>
|
|||
public class RoutedViewHost : UserControl, IActivatable, IEnableLogger |
|||
{ |
|||
/// <summary>
|
|||
/// The router dependency property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<RoutingState> RouterProperty = |
|||
AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router)); |
|||
|
|||
/// <summary>
|
|||
/// The default content property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<object> DefaultContentProperty = |
|||
AvaloniaProperty.Register<RoutedViewHost, object>(nameof(DefaultContent)); |
|||
|
|||
/// <summary>
|
|||
/// Fade in animation property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<IAnimation> FadeInAnimationProperty = |
|||
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent), |
|||
CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); |
|||
|
|||
/// <summary>
|
|||
/// Fade out animation property.
|
|||
/// </summary>
|
|||
public static readonly AvaloniaProperty<IAnimation> FadeOutAnimationProperty = |
|||
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent), |
|||
CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
|
|||
/// </summary>
|
|||
public RoutedViewHost() |
|||
{ |
|||
this.WhenActivated(disposables => |
|||
{ |
|||
this.WhenAnyObservable(x => x.Router.CurrentViewModel) |
|||
.DistinctUntilChanged() |
|||
.Subscribe(NavigateToViewModel) |
|||
.DisposeWith(disposables); |
|||
}); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the <see cref="RoutingState"/> of the view model stack.
|
|||
/// </summary>
|
|||
public RoutingState Router |
|||
{ |
|||
get => GetValue(RouterProperty); |
|||
set => SetValue(RouterProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the content displayed whenever there is no page currently routed.
|
|||
/// </summary>
|
|||
public object DefaultContent |
|||
{ |
|||
get => GetValue(DefaultContentProperty); |
|||
set => SetValue(DefaultContentProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the animation played when page appears.
|
|||
/// </summary>
|
|||
public IAnimation FadeInAnimation |
|||
{ |
|||
get => GetValue(FadeInAnimationProperty); |
|||
set => SetValue(FadeInAnimationProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the animation played when page disappears.
|
|||
/// </summary>
|
|||
public IAnimation FadeOutAnimation |
|||
{ |
|||
get => GetValue(FadeOutAnimationProperty); |
|||
set => SetValue(FadeOutAnimationProperty, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Duplicates the Content property with a private setter.
|
|||
/// </summary>
|
|||
public new object Content |
|||
{ |
|||
get => base.Content; |
|||
private set => base.Content = value; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the ReactiveUI view locator used by this router.
|
|||
/// </summary>
|
|||
public IViewLocator ViewLocator { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Invoked when ReactiveUI router navigates to a view model.
|
|||
/// </summary>
|
|||
/// <param name="viewModel">ViewModel to which the user navigates.</param>
|
|||
/// <exception cref="Exception">
|
|||
/// Thrown when ViewLocator is unable to find the appropriate view.
|
|||
/// </exception>
|
|||
private void NavigateToViewModel(IRoutableViewModel viewModel) |
|||
{ |
|||
if (viewModel == null) |
|||
{ |
|||
this.Log().Info("ViewModel is null, falling back to default content."); |
|||
UpdateContent(DefaultContent); |
|||
return; |
|||
} |
|||
|
|||
var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; |
|||
var view = viewLocator.ResolveView(viewModel); |
|||
if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); |
|||
|
|||
this.Log().Info($"Ready to show {view} with autowired {viewModel}."); |
|||
view.ViewModel = viewModel; |
|||
UpdateContent(view); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Updates the content with transitions.
|
|||
/// </summary>
|
|||
/// <param name="newContent">New content to set.</param>
|
|||
private async void UpdateContent(object newContent) |
|||
{ |
|||
if (FadeOutAnimation != null) |
|||
await FadeOutAnimation.RunAsync(this, Clock); |
|||
Content = newContent; |
|||
if (FadeInAnimation != null) |
|||
await FadeInAnimation.RunAsync(this, Clock); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates opacity animation for this routed view host.
|
|||
/// </summary>
|
|||
/// <param name="from">Opacity to start from.</param>
|
|||
/// <param name="to">Opacity to finish with.</param>
|
|||
/// <param name="duration">Duration of the animation.</param>
|
|||
/// <returns>Animation object instance.</returns>
|
|||
private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) |
|||
{ |
|||
return new Avalonia.Animation.Animation |
|||
{ |
|||
Duration = duration, |
|||
Children = |
|||
{ |
|||
new KeyFrame |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter |
|||
{ |
|||
Property = OpacityProperty, |
|||
Value = from |
|||
} |
|||
}, |
|||
Cue = new Cue(0d) |
|||
}, |
|||
new KeyFrame |
|||
{ |
|||
Setters = |
|||
{ |
|||
new Setter |
|||
{ |
|||
Property = OpacityProperty, |
|||
Value = to |
|||
} |
|||
}, |
|||
Cue = new Cue(1d) |
|||
} |
|||
} |
|||
}; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,196 @@ |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Layout.UnitTests |
|||
{ |
|||
public class LayoutQueueTests |
|||
{ |
|||
[Fact] |
|||
public void Should_Enqueue() |
|||
{ |
|||
var target = new LayoutQueue<string>(_ => true); |
|||
var refQueue = new Queue<string>(); |
|||
var items = new[] { "1", "2", "3" }; |
|||
|
|||
foreach (var item in items) |
|||
{ |
|||
target.Enqueue(item); |
|||
refQueue.Enqueue(item); |
|||
} |
|||
|
|||
Assert.Equal(refQueue, target); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Dequeue() |
|||
{ |
|||
var target = new LayoutQueue<string>(_ => true); |
|||
var refQueue = new Queue<string>(); |
|||
var items = new[] { "1", "2", "3" }; |
|||
|
|||
foreach (var item in items) |
|||
{ |
|||
target.Enqueue(item); |
|||
refQueue.Enqueue(item); |
|||
} |
|||
|
|||
while (refQueue.Count > 0) |
|||
{ |
|||
Assert.Equal(refQueue.Dequeue(), target.Dequeue()); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Enqueue_UniqueElements() |
|||
{ |
|||
var target = new LayoutQueue<string>(_ => true); |
|||
|
|||
var items = new[] { "1", "2", "3", "1" }; |
|||
|
|||
foreach (var item in items) |
|||
{ |
|||
target.Enqueue(item); |
|||
} |
|||
|
|||
Assert.Equal(3, target.Count); |
|||
Assert.Equal(items.Take(3), target); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Shouldnt_Enqueue_More_Than_Limit_In_Loop() |
|||
{ |
|||
var target = new LayoutQueue<string>(_ => true); |
|||
|
|||
//1
|
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(1, target.Count); |
|||
|
|||
target.BeginLoop(3); |
|||
|
|||
target.Dequeue(); |
|||
|
|||
//2
|
|||
target.Enqueue("Foo"); |
|||
target.Dequeue(); |
|||
|
|||
//3
|
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(1, target.Count); |
|||
|
|||
target.Dequeue(); |
|||
|
|||
//4 more than limit shouldn't be added
|
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(0, target.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Shouldnt_Count_Unique_Enqueue_For_Limit_In_Loop() |
|||
{ |
|||
var target = new LayoutQueue<string>(_ => true); |
|||
|
|||
//1
|
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(1, target.Count); |
|||
|
|||
target.BeginLoop(3); |
|||
|
|||
target.Dequeue(); |
|||
|
|||
//2
|
|||
target.Enqueue("Foo"); |
|||
target.Enqueue("Foo"); |
|||
target.Dequeue(); |
|||
|
|||
//3
|
|||
target.Enqueue("Foo"); |
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(1, target.Count); |
|||
|
|||
target.Dequeue(); |
|||
|
|||
//4 more than limit shouldn't be added
|
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(0, target.Count); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Enqueue_When_Condition_True_After_Loop_When_Limit_Met() |
|||
{ |
|||
var target = new LayoutQueue<string>(_ => true); |
|||
|
|||
//1
|
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(1, target.Count); |
|||
|
|||
target.BeginLoop(3); |
|||
|
|||
target.Dequeue(); |
|||
|
|||
//2
|
|||
target.Enqueue("Foo"); |
|||
target.Dequeue(); |
|||
|
|||
//3
|
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(1, target.Count); |
|||
|
|||
target.Dequeue(); |
|||
|
|||
//4 more than limit shouldn't be added to queue
|
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(0, target.Count); |
|||
|
|||
target.EndLoop(); |
|||
|
|||
//after loop should be added once
|
|||
Assert.Equal(1, target.Count); |
|||
Assert.Equal("Foo", target.First()); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Shouldnt_Enqueue_When_Condition_False_After_Loop_When_Limit_Met() |
|||
{ |
|||
var target = new LayoutQueue<string>(_ => false); |
|||
|
|||
//1
|
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(1, target.Count); |
|||
|
|||
target.BeginLoop(3); |
|||
|
|||
target.Dequeue(); |
|||
|
|||
//2
|
|||
target.Enqueue("Foo"); |
|||
target.Dequeue(); |
|||
|
|||
//3
|
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(1, target.Count); |
|||
|
|||
target.Dequeue(); |
|||
|
|||
//4 more than limit shouldn't be added
|
|||
target.Enqueue("Foo"); |
|||
|
|||
Assert.Equal(0, target.Count); |
|||
|
|||
target.EndLoop(); |
|||
|
|||
Assert.Equal(0, target.Count); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,104 @@ |
|||
using System; |
|||
using System.Reactive.Concurrency; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.Platform; |
|||
using Avalonia.UnitTests; |
|||
using Avalonia; |
|||
using ReactiveUI; |
|||
using DynamicData; |
|||
using Xunit; |
|||
using Splat; |
|||
using Avalonia.Markup.Xaml; |
|||
using System.ComponentModel; |
|||
using System.Threading.Tasks; |
|||
using System.Reactive; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
public class RoutedViewHostTest |
|||
{ |
|||
public class FirstRoutableViewModel : ReactiveObject, IRoutableViewModel |
|||
{ |
|||
public string UrlPathSegment => "first"; |
|||
|
|||
public IScreen HostScreen { get; set; } |
|||
} |
|||
|
|||
public class FirstRoutableView : ReactiveUserControl<FirstRoutableViewModel> { } |
|||
|
|||
public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel |
|||
{ |
|||
public string UrlPathSegment => "second"; |
|||
|
|||
public IScreen HostScreen { get; set; } |
|||
} |
|||
|
|||
public class SecondRoutableView : ReactiveUserControl<SecondRoutableViewModel> { } |
|||
|
|||
public class ScreenViewModel : ReactiveObject, IScreen |
|||
{ |
|||
public RoutingState Router { get; } = new RoutingState(); |
|||
} |
|||
|
|||
public RoutedViewHostTest() |
|||
{ |
|||
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); |
|||
Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor<FirstRoutableViewModel>)); |
|||
Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor<SecondRoutableViewModel>)); |
|||
} |
|||
|
|||
[Fact] |
|||
public void RoutedViewHostShouldStayInSyncWithRoutingState() |
|||
{ |
|||
var screen = new ScreenViewModel(); |
|||
var defaultContent = new TextBlock(); |
|||
var host = new RoutedViewHost |
|||
{ |
|||
Router = screen.Router, |
|||
DefaultContent = defaultContent, |
|||
FadeOutAnimation = null, |
|||
FadeInAnimation = null |
|||
}; |
|||
|
|||
var root = new TestRoot |
|||
{ |
|||
Child = host |
|||
}; |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(TextBlock), host.Content.GetType()); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
|
|||
screen.Router.Navigate |
|||
.Execute(new FirstRoutableViewModel()) |
|||
.Subscribe(); |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); |
|||
|
|||
screen.Router.Navigate |
|||
.Execute(new SecondRoutableViewModel()) |
|||
.Subscribe(); |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(SecondRoutableView), host.Content.GetType()); |
|||
|
|||
screen.Router.NavigateBack |
|||
.Execute(Unit.Default) |
|||
.Subscribe(); |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); |
|||
|
|||
screen.Router.NavigateBack |
|||
.Execute(Unit.Default) |
|||
.Subscribe(); |
|||
|
|||
Assert.NotNull(host.Content); |
|||
Assert.Equal(typeof(TextBlock), host.Content.GetType()); |
|||
Assert.Equal(defaultContent, host.Content); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue