Skip to content

Commit

Permalink
Add interface for lazy children retrieval
Browse files Browse the repository at this point in the history
Behavior is unchanged, implementation will follow.

References #21
  • Loading branch information
andreashuber-lawo committed Apr 22, 2016
1 parent 2576ac7 commit ee1b4b0
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 33 deletions.
1 change: 1 addition & 0 deletions Lawo.EmberPlusSharp/Lawo.EmberPlusSharp.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
<Compile Include="Glow\GlowQualifiedFunction.cs" />
<Compile Include="Glow\GlowTypes.cs" />
<Compile Include="GlobalSuppressions.cs" />
<Compile Include="Model\ChildrenRequestPolicy.cs" />
<Compile Include="Model\ElementWithSchemas.cs" />
<Compile Include="Model\Function.cs" />
<Compile Include="Model\Function1.cs" />
Expand Down
31 changes: 31 additions & 0 deletions Lawo.EmberPlusSharp/Model/ChildrenRequestPolicy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// <copyright>Copyright 2012-2016 Lawo AG (http://www.lawo.com).</copyright>
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

namespace Lawo.EmberPlusSharp.Model
{
/// <summary>Represents the policy how children for a given node should be requested from the provider.</summary>
public enum ChildrenRequestPolicy
{
/// <summary>Do not request any children.</summary>
/// <remarks>No children will be requested as long as the <see cref="INode.ChildrenRequestPolicy"/>
/// property of an <see cref="INode"/> implementation has this value.</remarks>
None,

/// <summary>Request only direct children.</summary>
/// <remarks>If the <see cref="INode.ChildrenRequestPolicy"/> property of an <see cref="INode"/> implementation
/// has this value then only direct children were or will be requested for the node. The
/// <see cref="INode.ChildrenRequestPolicy"/> property of the direct children implementing <see cref="INode"/>
/// will have the initial value <see cref="None"/>.</remarks>
DirectOnly,

/// <summary>Request direct and indirect children.</summary>
/// <remarks>If the <see cref="INode.ChildrenRequestPolicy"/> property of an <see cref="INode"/> implementation
/// has this value, then all direct and indirect children were or will be requested for the node. The
/// <see cref="INode.ChildrenRequestPolicy"/> property of the direct and indirect children implementing
/// <see cref="INode"/> will have the value <see cref="All"/>.</remarks>
All
}
}
52 changes: 41 additions & 11 deletions Lawo.EmberPlusSharp/Model/Consumer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ public sealed class Consumer<TRoot> : IMonitoredConnection
{
private static readonly EmberData EmberDataCommand = new EmberData(0x01, 0x0A, 0x02);

private readonly TRoot root = Root<TRoot>.Construct(new Context(null, 0, string.Empty));
private readonly ReceiveQueue receiveQueue = new ReceiveQueue();
private readonly InvocationCollection pendingInvocations = new InvocationCollection();
private readonly StreamedParameterCollection streamedParameters = new StreamedParameterCollection();
private readonly TRoot root;
private readonly S101Client client;
private readonly int queryChildrenTimeout;
private readonly S101Message emberDataMessage;
Expand Down Expand Up @@ -86,11 +86,14 @@ public Task SendChangesAsync()
/// <exception cref="Exception">An exception was thrown from one of the callbacks passed to the
/// <see cref="S101Client"/> constructor, see <see cref="Exception.Message"/> for more information.</exception>
/// <exception cref="InvalidOperationException">This method was called from a thread other than the one that
/// executed <see cref="CreateAsync(S101Client, int, byte)"/>.</exception>
/// executed <see cref="CreateAsync(S101Client, int, ChildrenRequestPolicy, byte)"/>.</exception>
/// <exception cref="ObjectDisposedException"><see cref="Dispose"/> has been called or the connection has been
/// lost.</exception>
/// <exception cref="OperationCanceledException"><see cref="Dispose"/> has been called or the connection has
/// been lost.</exception>
/// <remarks>Also retrieves the children of any objects implementing <see cref="INode"/> that have had their
/// <see cref="INode.ChildrenRequestPolicy"/> property set to a value other than
/// <see cref="ChildrenRequestPolicy.None"/>.</remarks>
public async Task SendAsync()
{
if (this.root.HasChanges)
Expand Down Expand Up @@ -137,14 +140,36 @@ public static Task<Consumer<TRoot>> CreateAsync(S101Client client)
[SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "There's no other way.")]
public static Task<Consumer<TRoot>> CreateAsync(S101Client client, int timeout)
{
return CreateAsync(client, timeout, 0x00);
return CreateAsync(client, timeout, (byte)0x00);
}

/// <summary>Returns the return value of <see cref="CreateAsync(S101Client, int, ChildrenRequestPolicy, byte)">
/// CreateAsync(<paramref name="client"/>, <paramref name="timeout"/>, <paramref name="childrenRequestPolicy"/>,
/// 0x00)</see>.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "There's no other way.")]
public static Task<Consumer<TRoot>> CreateAsync(S101Client client, int timeout, ChildrenRequestPolicy childrenRequestPolicy)
{
return CreateAsync(client, timeout, childrenRequestPolicy, 0x00);
}

/// <summary>Returns the return value of <see cref="CreateAsync(S101Client, int, ChildrenRequestPolicy, byte)">
/// CreateAsync(<paramref name="client"/>, <paramref name="timeout"/>,
/// <see cref="ChildrenRequestPolicy.All">ChildrenRequestPolicy.All</see>, <paramref name="slot"/>)</see>.
/// </summary>
[SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "There's no other way.")]
public static Task<Consumer<TRoot>> CreateAsync(S101Client client, int timeout, byte slot)
{
return CreateAsync(client, timeout, ChildrenRequestPolicy.All, slot);
}

/// <summary>Asynchronously uses <paramref name="client"/> to create a new <see cref="Consumer{T}"/> object.
/// </summary>
/// <param name="client">The <see cref="S101Client"/> to use.</param>
/// <param name="timeout">The total amount of time, in milliseconds, this method will wait for the provider to
/// send all requested elements. Specify -1 to wait indefinitely.</param>
/// <param name="childrenRequestPolicy">The policy that defines whether direct and indirect children are
/// retrieved from the provider before this method returns.</param>
/// <param name="slot">The slot to communicate with. All outgoing <see cref="S101Message"/> objects will have
/// their <see cref="S101Message.Slot"/> property set to this value. Incoming messages are ignored, if their
/// <see cref="S101Message.Slot"/> property does not match this value.</param>
Expand All @@ -160,18 +185,22 @@ public static Task<Consumer<TRoot>> CreateAsync(S101Client client, int timeout)
/// <exception cref="TimeoutException">The provider did not send all requested elements within the specified
/// <paramref name="timeout"/>.</exception>
/// <remarks>
/// <para>This method returns when initial values have been received for all non-optional
/// <typeparamref name="TRoot"/> properties and recursively for all non-optional properties of
/// <see cref="FieldNode{T}"/> subclass objects. Afterwards, all changes are continuously synchronized such that
/// the state of the object tree accessible through the <see cref="Root"/> property mirrors the state of the
/// tree held by the provider.</para>
/// <para>Sets the <see cref="INode.ChildrenRequestPolicy"/> property of the <see cref="Root"/> object to the
/// value passed for <paramref name="childrenRequestPolicy"/> and then retrieves a partial or full copy of the
/// provider tree before returning the <see cref="Consumer{TRoot}"/> object. Exactly what elements are initially
/// requested from the provider depends on the type of <typeparamref name="TRoot"/> and the value of
/// <paramref name="childrenRequestPolicy"/>.</para>
/// <para>Afterwards, all changes are continuously synchronized such that the state of the object tree
/// accessible through the <see cref="Root"/> property mirrors the state of the tree held by the provider.
/// </para>
/// <para>All changes to the object tree are reported by raising the
/// <see cref="INotifyPropertyChanged.PropertyChanged"/> event of the affected objects.</para>
/// </remarks>
[SuppressMessage("Microsoft.Design", "CA1000:DoNotDeclareStaticMembersOnGenericTypes", Justification = "There's no other way.")]
public static async Task<Consumer<TRoot>> CreateAsync(S101Client client, int timeout, byte slot)
public static async Task<Consumer<TRoot>> CreateAsync(
S101Client client, int timeout, ChildrenRequestPolicy childrenRequestPolicy, byte slot)
{
var result = new Consumer<TRoot>(client, timeout, slot);
var result = new Consumer<TRoot>(client, timeout, childrenRequestPolicy, slot);
await result.QueryChildrenAsync();
result.ReceiveLoop();
result.AutoSendLoop();
Expand All @@ -194,8 +223,9 @@ public static async Task<Consumer<TRoot>> CreateAsync(S101Client client, int tim

////////////////////////////////////////////////////////////////////////////////////////////////////////////////

private Consumer(S101Client client, int timeout, byte slot)
private Consumer(S101Client client, int timeout, ChildrenRequestPolicy childrenRequestPolicy, byte slot)
{
this.root = Root<TRoot>.Construct(new Context(null, 0, string.Empty, childrenRequestPolicy));
this.client = client;
this.queryChildrenTimeout = timeout;
this.emberDataMessage = new S101Message(slot, EmberDataCommand);
Expand Down
33 changes: 13 additions & 20 deletions Lawo.EmberPlusSharp/Model/Context.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,25 @@ namespace Lawo.EmberPlusSharp.Model
{
internal sealed class Context
{
private readonly IParent parent;
private readonly int number;
private readonly string identifier;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////

internal Context(IParent parent, int number, string identifier)
internal Context(IParent parent, int number, string identifier) :
this(parent, number, identifier, ChildrenRequestPolicy.All)
{
this.parent = parent;
this.number = number;
this.identifier = identifier;
}

internal IParent Parent
internal Context(IParent parent, int number, string identifier, ChildrenRequestPolicy childrenRequestPolicy)
{
get { return this.parent; }
this.Parent = parent;
this.Number = number;
this.Identifier = identifier;
this.ChildrenRequestPolicy = childrenRequestPolicy;
}

internal int Number
{
get { return this.number; }
}
internal IParent Parent { get; }

internal string Identifier
{
get { return this.identifier; }
}
internal int Number { get; }

internal string Identifier { get; }

internal ChildrenRequestPolicy ChildrenRequestPolicy { get; }
}
}
2 changes: 1 addition & 1 deletion Lawo.EmberPlusSharp/Model/Element.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ internal virtual RequestState RequestState
set { } // Intentionally empty
}

internal void SetContext(Context context)
internal virtual void SetContext(Context context)
{
this.parent = context.Parent;

Expand Down
11 changes: 11 additions & 0 deletions Lawo.EmberPlusSharp/Model/INode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace Lawo.EmberPlusSharp.Model
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

Expand All @@ -16,6 +17,16 @@ public interface INode : IElementWithSchemas
/// <summary>Gets a value indicating whether this is a root node.</summary>
bool IsRoot { get; }

/// <summary>Gets or sets the policy for this node.</summary>
/// <exception cref="ArgumentException">Attempted to set a new value when the current value is not equal to
/// <see cref="ChildrenRequestPolicy.None"/>.</exception>
/// <exception cref="ArgumentOutOfRangeException">Attempted to set a value that is not equal to one of the named
/// constants of <see cref="ChildrenRequestPolicy"/>.</exception>
/// <remarks>Setting this property prompts the consumer to automatically request children according to the new
/// value. To wait for the children to be retrieved, <see langword="await"/> the result of a call to
/// <see cref="Consumer{TRoot}.SendAsync"/>.</remarks>
ChildrenRequestPolicy ChildrenRequestPolicy { get; set; }

/// <summary>Gets the children of this node.</summary>
ReadOnlyObservableCollection<IElement> Children { get; }

Expand Down
44 changes: 43 additions & 1 deletion Lawo.EmberPlusSharp/Model/NodeBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace Lawo.EmberPlusSharp.Model
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
Expand All @@ -24,6 +25,38 @@ public abstract class NodeBase<TMostDerived> : ElementWithSchemas<TMostDerived>,
where TMostDerived : NodeBase<TMostDerived>
{
private readonly SortedDictionary<int, Element> children = new SortedDictionary<int, Element>();
private ChildrenRequestPolicy childrenRequestPolicy;

////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/// <inheritdoc/>
public ChildrenRequestPolicy ChildrenRequestPolicy
{
get
{
return this.childrenRequestPolicy;
}

set
{
if ((value < ChildrenRequestPolicy.None) || (value > ChildrenRequestPolicy.All))
{
throw new ArgumentOutOfRangeException("value");
}

if (value != this.childrenRequestPolicy)
{
if (this.childrenRequestPolicy != ChildrenRequestPolicy.None)
{
throw new ArgumentException(
"A new value cannot be set if the current value is not equal to ChildrenRequestPolicy.None.",
"value");
}

this.SetValue(ref this.childrenRequestPolicy, value);
}
}
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////

Expand Down Expand Up @@ -51,6 +84,12 @@ internal NodeBase() : base(RequestState.None)
{
}

internal sealed override void SetContext(Context context)
{
base.SetContext(context);
this.childrenRequestPolicy = context.ChildrenRequestPolicy;
}

internal IElement GetChild(int number)
{
return this.children[number];
Expand Down Expand Up @@ -336,7 +375,10 @@ private void ReadChildContents(
using (var contentsReader = new EmberReader(stream))
{
contentsReader.Read(); // Read what we have written with WriteStartSet above
var context = new Context(this, number, identifier);

var newPolicy = this.childrenRequestPolicy == ChildrenRequestPolicy.All ?
ChildrenRequestPolicy.All : ChildrenRequestPolicy.None;
var context = new Context(this, number, identifier, newPolicy);
child = this.ReadNewChildContents(contentsReader, actualType, context, out childRequestState);

if (child != null)
Expand Down
5 changes: 5 additions & 0 deletions Lawo.EmberPlusSharpTest/Lawo.EmberPlusSharpTest.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,11 @@
<ItemGroup>
<EmbeddedResource Include="Model\Test\EmberDataPayloads\StreamLog.xml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Model\Test\EmberDataPayloads\ChildrenRequestStateLog.xml">
<SubType>Designer</SubType>
</EmbeddedResource>
</ItemGroup>
<Choose>
<When Condition="'$(VisualStudioVersion)' == '10.0' And '$(IsCodedUITest)' == 'True'">
<ItemGroup>
Expand Down
52 changes: 52 additions & 0 deletions Lawo.EmberPlusSharpTest/Model/ConsumerTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,19 @@ public void AccessTest()
"AccessLog.xml"));
}

/// <summary>Tests whether <see cref="INode.ChildrenRequestPolicy"/> works as expected.</summary>
[TestMethod]
public void ChildrenRequestPolicyTest()
{
AsyncPump.Run(
async () =>
{
await ChildrenRequestPolicyTestCoreAsync(ChildrenRequestPolicy.None);
await ChildrenRequestPolicyTestCoreAsync(ChildrenRequestPolicy.DirectOnly);
await ChildrenRequestPolicyTestCoreAsync(ChildrenRequestPolicy.All);
});
}

/// <summary>Tests nullable parameter variants.</summary>
[TestMethod]
public void NullableTest()
Expand Down Expand Up @@ -1561,6 +1574,45 @@ private static void AssertAccess(RecursiveFieldNode node)
}
}

private static Task ChildrenRequestPolicyTestCoreAsync(ChildrenRequestPolicy policy)
{
return TestWithRobot<ModelPayloads>(
async client =>
{
using (var consumer = await Consumer<EmptyNodeRoot>.CreateAsync(client, 4000, policy))
{
var root = consumer.Root;
AssertThrow<ArgumentOutOfRangeException>(
() => root.ChildrenRequestPolicy = (ChildrenRequestPolicy)(-1),
() => root.ChildrenRequestPolicy = ChildrenRequestPolicy.All + 1);
Assert.AreEqual(policy, root.ChildrenRequestPolicy);

const string expectedMessage =
"A new value cannot be set if the current value is not equal to ChildrenRequestPolicy.None.\r\nParameter name: value";

if (root.ChildrenRequestPolicy == ChildrenRequestPolicy.None)
{
root.ChildrenRequestPolicy = ChildrenRequestPolicy.DirectOnly;
AssertThrow<ArgumentException>(
() => root.ChildrenRequestPolicy = ChildrenRequestPolicy.All, expectedMessage);
}
else
{
AssertThrow<ArgumentException>(() => root.ChildrenRequestPolicy -= 1, expectedMessage);
}

var childPolicy = policy == ChildrenRequestPolicy.All ?
ChildrenRequestPolicy.All : ChildrenRequestPolicy.None;
Assert.AreEqual(childPolicy, root.Node.ChildrenRequestPolicy);
}
},
null,
null,
GlowTypes.Instance,
false,
"ChildrenRequestStateLog.xml");
}

[SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "Objects are disposed within the called method.")]
private static Task TestWithRobot<TRoot>(
Func<Consumer<TRoot>, Task> testCallback, bool log, string logXmlName, params object[] args) where TRoot : Root<TRoot>
Expand Down
Loading

0 comments on commit ee1b4b0

Please sign in to comment.