Merge branch 'release/0.9.5'

This commit is contained in:
Antony Male 2014-04-24 13:00:38 +01:00
commit f32e83cbb5
20 changed files with 245 additions and 121 deletions

View File

@ -1,6 +1,13 @@
Stylet Changelog Stylet Changelog
================ ================
v0.9.5
------
- Add StyletConductorItemsControl style, for using an ItemsControl with a Conductor<T>.Collection.AllActive
- Make Execute.OnUIThread synchronous, and add BeginOnUIThread (asynchronous)
- Rename conductors. It's now Conductor<T>.Collection.xxx not Conductor<T>.Collections.xxx
v0.9.4 v0.9.4
------ ------

View File

@ -2,7 +2,7 @@
<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd"> <package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd">
<metadata> <metadata>
<id>Stylet</id> <id>Stylet</id>
<version>0.9.4</version> <version>0.9.5</version>
<title>Stylet</title> <title>Stylet</title>
<authors>Antony Male</authors> <authors>Antony Male</authors>
<owners>Antony Male</owners> <owners>Antony Male</owners>

View File

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace Stylet.Samples.RedditBrowser.Pages namespace Stylet.Samples.RedditBrowser.Pages
{ {
public class ShellViewModel : Conductor<IScreen>.Collections.OneActive, IHandle<OpenSubredditEvent> public class ShellViewModel : Conductor<IScreen>.Collection.OneActive, IHandle<OpenSubredditEvent>
{ {
private ISubredditViewModelFactory subredditViewModelFactory; private ISubredditViewModelFactory subredditViewModelFactory;

View File

@ -7,7 +7,7 @@ using System.Threading.Tasks;
namespace Stylet.Samples.RedditBrowser.Pages namespace Stylet.Samples.RedditBrowser.Pages
{ {
public class SubredditViewModel : Conductor<IScreen>.Collections.Navigation public class SubredditViewModel : Conductor<IScreen>.StackNavigation
{ {
private IPostCommentsViewModelFactory postCommentsViewModelFactory; private IPostCommentsViewModelFactory postCommentsViewModelFactory;

View File

@ -6,7 +6,7 @@ using System.Threading.Tasks;
namespace Stylet.Samples.TabNavigation namespace Stylet.Samples.TabNavigation
{ {
class ShellViewModel : Conductor<IScreen>.Collections.OneActive class ShellViewModel : Conductor<IScreen>.Collection.OneActive
{ {
public ShellViewModel(Page1ViewModel page1, Page2ViewModel page2) public ShellViewModel(Page1ViewModel page1, Page2ViewModel page2)
{ {

View File

@ -10,7 +10,7 @@ namespace Stylet
{ {
public partial class Conductor<T> public partial class Conductor<T>
{ {
public partial class Collections public partial class Collection
{ {
/// <summary> /// <summary>
/// Conductor which has many items, all of which active at the same time /// Conductor which has many items, all of which active at the same time

View File

@ -8,106 +8,103 @@ namespace Stylet
{ {
public partial class Conductor<T> public partial class Conductor<T>
{ {
public partial class Collections
{
/// <summary> /// <summary>
/// Stack-based navigation. A Conductor which has one active item, and a stack of previous items /// Stack-based navigation. A Conductor which has one active item, and a stack of previous items
/// </summary> /// </summary>
public class Navigation : ConductorBaseWithActiveItem<T> public class StackNavigation : ConductorBaseWithActiveItem<T>
{
// We need to remove arbitrary items, so no Stack<T> here!
private List<T> history = new List<T>();
/// <summary>
/// Activate the given item. This deactivates the previous item, and pushes it onto the history stack
/// </summary>
/// <param name="item">Item to activate</param>
public override void ActivateItem(T item)
{ {
// We need to remove arbitrary items, so no Stack<T> here! if (item != null && item.Equals(this.ActiveItem))
private List<T> history = new List<T>();
/// <summary>
/// Activate the given item. This deactivates the previous item, and pushes it onto the history stack
/// </summary>
/// <param name="item">Item to activate</param>
public override void ActivateItem(T item)
{ {
if (item != null && item.Equals(this.ActiveItem)) if (this.IsActive)
ScreenExtensions.TryActivate(this.ActiveItem);
}
else
{
if (this.ActiveItem != null)
this.history.Add(this.ActiveItem);
this.ChangeActiveItem(item, false);
}
}
/// <summary>
/// Deactivate the given item
/// </summary>
/// <param name="item">Item to deactivate</param>
public override void DeactivateItem(T item)
{
ScreenExtensions.TryDeactivate(item);
}
/// <summary>
/// Close the active item, and re-activate the top item in the history stack
/// </summary>
public void GoBack()
{
this.CloseItem(this.ActiveItem);
}
/// <summary>
/// Close and remove all items in the history stack, leaving the ActiveItem
/// </summary>
public void Clear()
{
foreach (var item in this.history)
this.CloseAndCleanUp(item);
this.history.Clear();
}
/// <summary>
/// Close the given item. If it was the ActiveItem, activate the top item in the history stack
/// </summary>
/// <param name="item"></param>
public override async void CloseItem(T item)
{
if (item == null || !await this.CanCloseItem(item))
return;
if (item.Equals(this.ActiveItem))
{
var newItem = default(T);
if (this.history.Count > 0)
{ {
if (this.IsActive) newItem = this.history.Last();
ScreenExtensions.TryActivate(this.ActiveItem); this.history.RemoveAt(this.history.Count - 1);
}
else
{
if (this.ActiveItem != null)
this.history.Add(this.ActiveItem);
this.ChangeActiveItem(item, false);
} }
this.ChangeActiveItem(newItem, true);
} }
else if (this.history.Contains(item))
/// <summary>
/// Deactivate the given item
/// </summary>
/// <param name="item">Item to deactivate</param>
public override void DeactivateItem(T item)
{ {
ScreenExtensions.TryDeactivate(item); this.CloseAndCleanUp(item);
this.history.Remove(item);
} }
}
/// <summary> /// <summary>
/// Close the active item, and re-activate the top item in the history stack /// Returns true if and when all items (ActiveItem + everything in the history stack) can close
/// </summary> /// </summary>
public void GoBack() /// <returns></returns>
{ public override Task<bool> CanCloseAsync()
this.CloseItem(this.ActiveItem); {
} return this.CanAllItemsCloseAsync(this.history.Concat(new[] { this.ActiveItem }));
}
/// <summary> protected override void OnClose()
/// Close and remove all items in the history stack, leaving the ActiveItem {
/// </summary> // We've already been deactivated by this point
public void Clear() foreach (var item in this.history)
{ this.CloseAndCleanUp(item);
foreach (var item in this.history) this.history.Clear();
this.CloseAndCleanUp(item);
this.history.Clear();
}
/// <summary> this.CloseAndCleanUp(this.ActiveItem);
/// Close the given item. If it was the ActiveItem, activate the top item in the history stack
/// </summary>
/// <param name="item"></param>
public override async void CloseItem(T item)
{
if (item == null || !await this.CanCloseItem(item))
return;
if (item.Equals(this.ActiveItem))
{
var newItem = default(T);
if (this.history.Count > 0)
{
newItem = this.history.Last();
this.history.RemoveAt(this.history.Count-1);
}
this.ChangeActiveItem(newItem, true);
}
else if (this.history.Contains(item))
{
this.CloseAndCleanUp(item);
this.history.Remove(item);
}
}
/// <summary>
/// Returns true if and when all items (ActiveItem + everything in the history stack) can close
/// </summary>
/// <returns></returns>
public override Task<bool> CanCloseAsync()
{
return this.CanAllItemsCloseAsync(this.history.Concat(new[] { this.ActiveItem }));
}
protected override void OnClose()
{
// We've already been deactivated by this point
foreach (var item in this.history)
this.CloseAndCleanUp(item);
this.history.Clear();
this.CloseAndCleanUp(this.ActiveItem);
}
} }
} }
} }

View File

@ -10,7 +10,7 @@ namespace Stylet
{ {
public partial class Conductor<T> public partial class Conductor<T>
{ {
public partial class Collections public partial class Collection
{ {
/// <summary> /// <summary>
/// Conductor with many items, only one of which is active /// Conductor with many items, only one of which is active

View File

@ -12,24 +12,90 @@ namespace Stylet
{ {
public static class Execute public static class Execute
{ {
/// <summary>
/// Should be set to the UI thread's SynchronizationContext. This is normally done by the Bootstrapper.
/// </summary>
public static SynchronizationContext SynchronizationContext; public static SynchronizationContext SynchronizationContext;
/// <summary>
/// FOR TESTING ONLY. Causes everything to execute synchronously
/// </summary>
public static bool TestExecuteSynchronously = false;
private static bool? inDesignMode; private static bool? inDesignMode;
public static Action<Action> DefaultPropertyChangedDispatcher = Execute.OnUIThread; /// <summary>
/// Default dispatcher used by PropertyChangedBase instances. Defaults to BeginOnUIThreadOrSynchronous
/// </summary>
public static Action<Action> DefaultPropertyChangedDispatcher = Execute.BeginOnUIThreadOrSynchronous;
public static void OnUIThread(Action action) private static void EnsureSynchronizationContext()
{ {
// If we're already on the given SynchronizationContext, or it hasn't been set, run synchronously if (SynchronizationContext == null && !TestExecuteSynchronously)
if (SynchronizationContext != null && SynchronizationContext != SynchronizationContext.Current) throw new Exception("Execute.SynchronizationContext must be set before this method can be called. This should normally have been done by the Bootstrapper");
}
/// <summary>
/// Dispatches the given action to be run on the UI thread, even if the current thread is the UI thread
/// </summary>
public static void BeginOnUIThread(Action action)
{
EnsureSynchronizationContext();
if (!TestExecuteSynchronously)
SynchronizationContext.Post(_ => action(), null); SynchronizationContext.Post(_ => action(), null);
else else
action(); action();
} }
/// <summary>
/// Dispatches the given action to be run on the UI thread, or runs it synchronously if the current thread is the UI thread
/// </summary>
public static void BeginOnUIThreadOrSynchronous(Action action)
{
EnsureSynchronizationContext();
if (SynchronizationContext != SynchronizationContext.Current && !TestExecuteSynchronously)
SynchronizationContext.Post(_ => action(), null);
else
action();
}
/// <summary>
/// Dispatches the given action to be run on the UI thread and blocks until it completes, or runs it synchronously if the current thread is the UI thread
/// </summary>
public static void OnUIThread(Action action)
{
EnsureSynchronizationContext();
Exception exception = null;
if (SynchronizationContext != SynchronizationContext.Current && !TestExecuteSynchronously)
{
SynchronizationContext.Send(_ =>
{
try
{
action();
}
catch (Exception e)
{
exception = e;
}
}, null);
if (exception != null)
throw new System.Reflection.TargetInvocationException("An error occurred while dispatching a call to the UI Thread", exception);
}
else
{
action();
}
}
/// <summary>
/// Dispatches the given action to be run on the UI thread and returns a task that completes when the action completes, or runs it synchronously if the current thread is the UI thread
/// </summary>
public static Task OnUIThreadAsync(Action action) public static Task OnUIThreadAsync(Action action)
{ {
// If we're already on the given SynchronizationContext, or it hasn't been set, run synchronously EnsureSynchronizationContext();
if (SynchronizationContext != null && SynchronizationContext != SynchronizationContext.Current) if (SynchronizationContext != SynchronizationContext.Current && !TestExecuteSynchronously)
{ {
var tcs = new TaskCompletionSource<object>(); var tcs = new TaskCompletionSource<object>();
SynchronizationContext.Post(_ => SynchronizationContext.Post(_ =>

View File

@ -35,5 +35,5 @@ using System.Windows.Markup;
// You can specify all the values or you can default the Build and Revision Numbers // You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below: // by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")] // [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.9.4.0")] [assembly: AssemblyVersion("0.9.5.0")]
[assembly: AssemblyFileVersion("0.9.4.0")] [assembly: AssemblyFileVersion("0.9.5.0")]

View File

@ -64,7 +64,7 @@ namespace Stylet
var propertyAsEventInfo = valueService.TargetProperty as EventInfo; var propertyAsEventInfo = valueService.TargetProperty as EventInfo;
if (propertyAsEventInfo != null) if (propertyAsEventInfo != null)
{ {
var ec = new EventAction((DependencyObject)valueService.TargetObject, propertyAsEventInfo, this.Method, this.NullTarget.GetValueOrDefault(ActionUnavailableBehaviour.Throw), this.ActionNotFound.GetValueOrDefault(ActionUnavailableBehaviour.Throw)); var ec = new EventAction((DependencyObject)valueService.TargetObject, propertyAsEventInfo, this.Method, this.NullTarget.GetValueOrDefault(ActionUnavailableBehaviour.Enable), this.ActionNotFound.GetValueOrDefault(ActionUnavailableBehaviour.Throw));
return ec.GetDelegate(); return ec.GetDelegate();
} }

View File

@ -13,4 +13,15 @@
</Setter.Value> </Setter.Value>
</Setter> </Setter>
</Style> </Style>
<Style x:Key="StyletConductorItemsControl" TargetType="ItemsControl">
<Setter Property="ItemsSource" Value="{Binding Items}"/>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<ContentControl s:View.Model="{Binding}" VerticalContentAlignment="Stretch" HorizontalContentAlignment="Stretch" IsTabStop="False"/>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary> </ResourceDictionary>

View File

@ -16,6 +16,12 @@ namespace StyletUnitTests
{ {
private class Element { } private class Element { }
[TestFixtureSetUp]
public void SetUpFixture()
{
Execute.TestExecuteSynchronously = true;
}
[Test] [Test]
public void AddRangeAddsElements() public void AddRangeAddsElements()
{ {

View File

@ -12,12 +12,18 @@ namespace StyletUnitTests
[TestFixture] [TestFixture]
public class ConductorAllActiveTests public class ConductorAllActiveTests
{ {
private Conductor<IScreen>.Collections.AllActive conductor; private Conductor<IScreen>.Collection.AllActive conductor;
[TestFixtureSetUp]
public void SetUpFixture()
{
Execute.TestExecuteSynchronously = true;
}
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
this.conductor = new Conductor<IScreen>.Collections.AllActive(); this.conductor = new Conductor<IScreen>.Collection.AllActive();
} }
[Test] [Test]

View File

@ -12,12 +12,12 @@ namespace StyletUnitTests
[TestFixture] [TestFixture]
public class ConductorNavigatingTests public class ConductorNavigatingTests
{ {
private Conductor<IScreen>.Collections.Navigation conductor; private Conductor<IScreen>.StackNavigation conductor;
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
this.conductor = new Conductor<IScreen>.Collections.Navigation(); this.conductor = new Conductor<IScreen>.StackNavigation();
} }
[Test] [Test]

View File

@ -12,12 +12,18 @@ namespace StyletUnitTests
[TestFixture] [TestFixture]
public class ConductorOneActiveTests public class ConductorOneActiveTests
{ {
private Conductor<IScreen>.Collections.OneActive conductor; private Conductor<IScreen>.Collection.OneActive conductor;
[TestFixtureSetUp]
public void SetUpFixture()
{
Execute.TestExecuteSynchronously = true;
}
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {
this.conductor = new Conductor<IScreen>.Collections.OneActive(); this.conductor = new Conductor<IScreen>.Collection.OneActive();
} }
[Test] [Test]

View File

@ -13,23 +13,21 @@ namespace StyletUnitTests
[TestFixture] [TestFixture]
public class ExecuteTests public class ExecuteTests
{ {
[Test] [TestFixtureSetUp]
public void OnUIThreadExecutesSynchronouslyIfNoSynchronizationContext() public void SetUpFixture()
{ {
Execute.SynchronizationContext = null; // Dont want this being previously set by anything and messing us around
bool blockWasRun = false; Execute.TestExecuteSynchronously = false;
Execute.OnUIThread(() => blockWasRun = true);
Assert.IsTrue(blockWasRun);
} }
[Test] [Test]
public void OnUIThreadExecutesAsynchronouslyIfSynchronizationContextIsNotNull() public void OnUIThreadExecutesUsingSynchronizationContext()
{ {
var sync = new Mock<SynchronizationContext>(); var sync = new Mock<SynchronizationContext>();
Execute.SynchronizationContext = sync.Object; Execute.SynchronizationContext = sync.Object;
SendOrPostCallback passedAction = null; SendOrPostCallback passedAction = null;
sync.Setup(x => x.Post(It.IsAny<SendOrPostCallback>(), null)).Callback((SendOrPostCallback a, object o) => passedAction = a); sync.Setup(x => x.Send(It.IsAny<SendOrPostCallback>(), null)).Callback((SendOrPostCallback a, object o) => passedAction = a);
bool actionCalled = false; bool actionCalled = false;
Execute.OnUIThread(() => actionCalled = true); Execute.OnUIThread(() => actionCalled = true);
@ -40,12 +38,20 @@ namespace StyletUnitTests
} }
[Test] [Test]
public async Task OnUIThreadAsyncExecutesSynchronouslyIfNoSynchronizationContext() public void BeginOnUIThreadExecutesUsingSynchronizationContext()
{ {
Execute.SynchronizationContext = null; var sync = new Mock<SynchronizationContext>();
bool blockWasRun = false; Execute.SynchronizationContext = sync.Object;
await Execute.OnUIThreadAsync(() => blockWasRun = true);
Assert.IsTrue(blockWasRun); SendOrPostCallback passedAction = null;
sync.Setup(x => x.Post(It.IsAny<SendOrPostCallback>(), null)).Callback((SendOrPostCallback a, object o) => passedAction = a);
bool actionCalled = false;
Execute.BeginOnUIThread(() => actionCalled = true);
Assert.IsFalse(actionCalled);
passedAction(null);
Assert.IsTrue(actionCalled);
} }
[Test] [Test]

View File

@ -34,6 +34,12 @@ namespace StyletUnitTests
} }
} }
[TestFixtureSetUp]
public void SetUpFixture()
{
Execute.TestExecuteSynchronously = true;
}
[Test] [Test]
public void RefreshRaisesPropertyChangedWithEmptyString() public void RefreshRaisesPropertyChangedWithEmptyString()
{ {

View File

@ -50,6 +50,12 @@ namespace StyletUnitTests
} }
} }
[TestFixtureSetUp]
public void SetUpFixture()
{
Execute.TestExecuteSynchronously = true;
}
[Test] [Test]
public void StrongBindingBinds() public void StrongBindingBinds()
{ {

View File

@ -48,6 +48,13 @@ namespace StyletUnitTests
private MyScreen screen; private MyScreen screen;
[TestFixtureSetUp]
public void SetUpFixture()
{
Execute.TestExecuteSynchronously = true;
}
[SetUp] [SetUp]
public void SetUp() public void SetUp()
{ {