Binding StringElement (MT.D) with MvvmCross - mvvmcross

We are using some MT.D StringElements, and their Value Property is bound to properties in the ViewModel.
The initial value is correctly shown but when the ViewModel changes some values and triggers PropertyChanged then the StringElements contain the good value but the display is not refreshed.
If we scroll the Controller or touch the StringElement then it is refreshed: the correct value is displayed.
Do you have any idea?
This is our ViewController
public class ContactView : MvxDialogViewController
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
var bindings = this.CreateInlineBindingTarget<ContactViewModel> ();
Root = new RootElement()
{
new Section()
{
new StringElement("Company Name").Bind(bindings, vm => vm.CompanyName)
}
}
}
}
This is our ViewModel (simplified)
public class ContactViewModel : MvxViewModel
{
private string companyName;
public string CompanyName{
get{return companyName;}
set{companyName = value; RaisePropertyChanged(() => CompanyName);}
}
public async Task Init(string id)
{
var contact = await someService.SomeMethodAsync();
CompanyName = contact.CompanyName;
}
}

I found two solutions to my problem:
If I use UIView.Transition to replace the content then, in the new View, nothing is refreshed when I change the ViewModel (unless I scroll or tap it) UNLESS if the ViewModel properties have some default value non null and non empty
If I don't transition but use another method like this one to replace the content:
Sample code
MasterNavigationController.PopToRootViewController(false);
MasterNavigationController.ViewControllers = new UIViewController[] { viewController };
In this case the content is replaced and the view is refreshed when a ViewModel property changes: everything works correctly in this case.

I tried a viewmodel like:
public class FirstViewModel
: MvxViewModel
{
private Timer _timer;
private int _count;
public FirstViewModel()
{
_timer = new Timer(DoThis, null, 1000, 1000);
}
private void DoThis(object state)
{
_count++;
TextProperty = _count.ToString();
}
private string _textProperty = "T";
public string TextProperty
{
get { return _textProperty; }
set { _textProperty = value; RaisePropertyChanged(() => TextProperty); }
}
}
with a dialog view defined like:
Root = new RootElement("Example Root")
{
new Section("Debut in:")
{
new EntryElement("Login", "Enter Login name").Bind(bindings, vm => vm.TextProperty)
},
new Section("Debug out:")
{
new StringElement("Value is:").Bind(bindings, vm => vm.TextProperty),
};
It worked fine - ticking up every second...

Related

My static variables in my Blazor Server app are keeping their values, even if I refresh the page or even I close the tab and login again. Why?

I have a Blazor server app. Some variables on a specific razor page (main.razor) are defined as static because I want that these variables keep their values when the client navigates to other pages in the same project and comes back again to main.razor. So far it is working good.
But when I refresh the complete page, or even close the tab and reopen my app (login again), I see that the static variables still keep their values. How can prevent this? Of course I want that the values return to their default values (like 0 or ""), when the client makes a login or refreshes the page with F5. How can I do that?
I have defined the related variables in the following way:
private static StringBuilder log = new StringBuilder();
public static string testvar1= "";
public static int testvar2= 0;
Statics exist for the lifetime of the application instance which explains the behaviour you see.
You need to be maintaining state. At one end of the spectrum you can implement a State Management system such as Fluxor. At the other just create a user class, set it up as a service and inject it as a Scoped Service. Or you can build a middle-of-the-road solution.
This is mine.
A generic UIStateService that maintains a Dictionary of (state)objects against a Guid.
public class UIStateService
{
private Dictionary<Guid, object> _stateItems = new Dictionary<Guid, object>();
public void AddStateData(Guid Id, object value)
{
if (_stateItems.ContainsKey(Id))
_stateItems[Id] = value;
else
_stateItems.Add(Id, value);
}
public void ClearStateData(Guid Id)
{
if (_stateItems.ContainsKey(Id))
_stateItems.Remove(Id);
}
public bool TryGetStateData<T>(Guid Id, out T? value)
{
value = default;
if (Id == Guid.Empty)
return false;
var isdata = _stateItems.ContainsKey(Id);
var val = isdata
? _stateItems[Id]
: default;
if (val is T)
{
value = (T)val;
return true;
}
return false;
}
}
Set it up as a service:
builder.Services.AddScoped<UIStateService>();
Next define a simple template ComponentBase page that contains the common page code:
using Blazr.UI;
using Microsoft.AspNetCore.Components;
namespace BlazorApp2.Pages
{
public class StatePage : ComponentBase
{
// this provides a guid for this specific page during the lifetime of the application runtime
// we use this as the reference to store the state data against
private static Guid RouteId = Guid.NewGuid();
[Inject] protected UIStateService UIStateService { get; set; } = default!;
protected void SaveState<T>(T state) where T : class, new()
{
if (RouteId != Guid.Empty)
this.UIStateService.AddStateData(RouteId, state);
}
protected bool GetState<T>( out T value) where T : class, new()
{
value = new T();
if (RouteId != Guid.Empty && this.UIStateService.TryGetStateData<T>(RouteId, out T? returnedState))
{
value = returnedState ?? new T();
return true;
}
else
return false;
}
}
}
And use it in a page:
#page "/"
#inherits StatePage
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div class="p-2">
<button class="btn btn-primary" #onclick=SetData>Set Data</button>
</div>
<div class="p-3 text-primary">
State Time : #stateData.StateTime;
</div>
#code {
private MyStateData stateData = new MyStateData();
protected override void OnInitialized()
{
if (this.GetState<MyStateData>(out MyStateData value))
this.stateData = value;
else
this.SaveState<MyStateData>(this.stateData);
}
private void SetData()
{
this.stateData.StateTime = DateTime.Now.ToLongTimeString();
SaveState<MyStateData>(this.stateData);
}
public class MyStateData
{
public string StateTime { get; set; } = DateTime.Now.ToLongTimeString();
}
}
You can now navigate around the application and the state will be maintained for the page.
You can apply an observer/notification pattern to the state object to trigger automatic state updates if you wish.

primefaces org.primefaces.component.diagram override

I have a problem with org.primefaces.component.diagram, i want to add an action when click on any overlay or connector, i make this using jquery, but the problem is that there is no identifier for the connection, after search i was able to get the ids of the 2 end points of the connection but if there is many connection between the same points then i cannot distinguish between them, i tried to override the diagram and add "connectionId" attribute on the connection but i got an exception in the front end :
Uncaught ReferenceError: connectionId590236 is not defined at eval (eval at (jquery.js.xhtml?ln=primefaces&v=5.2:14), :1:1488)
screenshot
The closet solution would be is to use setId on Element in the DefaultDiagramModel creation.
An example would be as the following:
Element elementA = new Element("A", "20em", "6em");
elementA.setId("element-a");
Element elementB = new Element("B", "10em", "18em");
elementB.setId("element-b");
Element elementC = new Element("C", "40em", "18em");
elementC.setId("element-c");
...
Since PrimeFaces doesn't provide the control you are searching for, and the original component comes from jsPlumb, you may rely on that to achieve what you are looking for.
First make sure that the <p:diagram> has a widgetVar value, es. diagramWV
An example would be the following:
$(document).ready(function () {
//timeout makes sure the component is initialized
setTimeout(function () {
for (var key in PF('diagramWV').canvas.getAllConnections()) {
if (PF('diagramWV').canvas.getAllConnections().hasOwnProperty(key)) {
//Elemenets Events
// on source just once
$(PF('diagramWV').canvas.getAllConnections()[key].source).off('click').on('click', function () {
console.log($(this).attr('id'))
});
// on target just once
$(PF('diagramWV').canvas.getAllConnections()[key].target).off('click').on('click', function () {
console.log($(this).attr('id'))
});
//Connection Event
PF('diagramWV').canvas.getAllConnections()[key].bind("click", function (conn) {
console.log("source " + conn.sourceId);
console.log("target " + conn.targetId);
});
}
}
}, 500);
});
Note: The canvas property of the widgetVar is the current instance of jsPlumbInstance
Here's an online demo, and a small working example on github.
finally i found an acceptable solution :
-> add an label overlay on the connection and set the identifier on it.
org.primefaces.model.diagram.Connection conn = new org.primefaces.model.diagram.Connection(
EndPointA, EndPointB);
LabelOverlay labelOverlay = new LabelOverlay(connection.getId(), "labelOverlayClass", 0.3);
conn.getOverlays().add(labelOverlay);
-> then add JS function to handle on dbclick action on the connection and get the id from its related overlay using the classes "._jsPlumb_overlay" and "._jsPlumb_hover"
<p:remoteCommand name="connectionClicked"
actionListener="#{yourBean.onConnectionDoubleClick}" />
<script type="text/javascript">
var connectionId;
$('._jsPlumb_connector').on('dblclick', function(e) {
$('._jsPlumb_overlay._jsPlumb_hover').each(function() {
connectionId = $(this).text();
});
connectionClicked([ { name : 'connectionId', value : connectionId } ]);
});
});
</script>
-> finally in the bean you extract the id and do whatever you want
public void onConnectionDoubleClick() {
Map<String, String> params = FacesContext.getCurrentInstance()
.getExternalContext().getRequestParameterMap();
String connectionId = params.get("connectionId");
if(StringUtils.isBlank(connectionId))
return;
.........
I was able to add a click event to Overlay by extending the primefaces Overlay class. If you make a change to the toJS() class (taking heavy inspiration from the Primefaces LabelOverLay) then you can write your own overlay with the jsplumb overlay constructor. Here's my implementation of a ClickableLabelOverlay.
public class ClickableLabelOverlay implements Overlay {
private String label;
private String styleClass;
private double location = 0.5;
private String onClick;
public ClickableLabelOverlay() {
}
public ClickableLabelOverlay(String label) {
this.label = label;
}
public ClickableLabelOverlay(String label, String styleClass, double location, String onClick) {
this(label);
this.styleClass = styleClass;
this.location = location;
this.onClick = onClick;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getStyleClass() {
return styleClass;
}
public void setStyleClass(String styleClass) {
this.styleClass = styleClass;
}
public double getLocation() {
return location;
}
public void setLocation(double location) {
this.location = location;
}
public String getOnClick() {
return onClick;
}
public void setOnClick(String onClick) {
this.onClick = onClick;
}
public String getType() {
return "Label";
}
public String toJS(StringBuilder sb) {
sb.append("['Label',{label:'").append(label).append("'");
if(styleClass != null) sb.append(",cssClass:'").append(styleClass).append("'");
if(location != 0.5) sb.append(",location:").append(location);
if(onClick != null) sb.append(",events:{click:function(labelOverlay, originalEvent){").append(onClick).append("}}");
sb.append("}]");
return sb.toString();
}
}
Put any javascript you want to execute inside of the onClick variable and it'll run when you click on the overlay. For convenience I added it to the set of default overlays for my diagram.
diagram.getDefaultConnectionOverlays().add(new ClickableLabelOverlay(...)

MvxSpinner in Mvvmcross

I have three MvxSpinners in my android view.
These spinners are binded to three different lists.
and Mode of data binding is TwoWay for these spinners.i.e. when this view is
displayed,all of these three spinners are get displayed with the predefined values.
When user change the value in first spinner,then second spinner will be clear and
get loaded with new values based on the selected value in first spinner.
How can I achieve this?
There's many ways to accomplish this, where the code placement is really up to you. Overall the idea would be to have a "SelectedItem" object that you can pass into your method and "Load" the next List.
Please keep in mind that this code is more traditional MVVM, but can easily be converted to MVVMCross equivalent. I believe all these types should be supported by MVVMCross.
private MyFirstObject _selectedFirstObject;
public MyFirstObject SelectedFirstObject
{
get { return _selectedFirstObject; }
set
{
_selectedFirstObject = value;
RaisePropertyChanged("SelectedFirstObject");
if(value != null)
LoadMySecondObjects(value);
}
}
private ObservableCollection<MyFirstObject> _myFirstObjects;
public ObservableCollection<MyFirstObject> MyFirstObjects
{
get { return _myFirstObjects; }
set
{
_myFirstObjects = value;
RaisePropertyChanged("MyFirstObjects");
}
}
private ObservableCollection<MySecondObject> _mySecondObjects;
public ObservableCollection<MySecondObject> MySecondObjects
{
get { return _mySecondObjects; }
set
{
_mySecondObjects = value;
RaisePropertyChanged("MySecondObjects");
}
}
public void LoadMySecondObjects(MyFirstObject current)
{
//Wherever you're pulling data from
MySecondObjects = MyDataService.GetAll(current);
}
protected void RaisePropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
I had the same problem but only if you add null value (as a default value) to your ItemsSource and try to reset SelectedItem to null. SelectedItem is changed in ViewModel but not in the spinner. In that case there's number of solutions but I used message to inform View to set selected item
public class SpinnerSelectionChanged : MvxMessage
{
public SpinnerSelectionChanged(object sender, string spinnerName, int position): base(sender)
{
SpinnerName = spinnerName;
Position = position;
}
public string SpinnerName { get; set; }
public int Position { get; set; }
}
in View
private void OnSpinnerSelectionChanged(SpinnerSelectionChanged obj)
{
switch (obj.SpinnerName)
{
case "City":
_spinnerCity.SetSelection(obj.Position);
break;
case "Office":
_spinnerOffice.SetSelection(obj.Position);
break;
}
}

Force WCF Rest client to use Json deserializer regardless of content-type

How to force WCF Rest client to use Json deserializer regardless of content-type?
I am invoking a REST based web service through WCF.
The service returns JSON body, but has content-type "Application/xml". The WCF framework is now giving me the XmlException.
public class MessageFormatter : IClientMessageFormatter
{
private readonly IClientMessageFormatter _formatter;
public MessageFormatter(IClientMessageFormatter formatter)
{
_formatter = formatter;
}
public object DeserializeReply(System.ServiceModel.Channels.Message message, object[] parameters)
{
return _formatter.DeserializeReply(message, parameters);
}
}
that _formatter.DeserializeReply is throwing XmlException. I can't find any example anywhere to force json deserialization on reply.
Edit - The "message" object when moused over is throwing "{... Error reading body: System.Xml.XmlException: The data at the root level is invalid. Line 1, position 1. ...}"
That same object in another one of my project that communicate with a different REST service (Picasa web services) has a what seems like a xml serialised version of JSON object?? So the problem seems further up the stream. I need to find where this object is originating from. I'll go play around with MessageEncoder class.
Edit - (Adding more info)
public class MyBinding : WebHttpBinding
{
public MyBinding(WebHttpSecurityMode mode)
: base(mode)
{
}
public override BindingElementCollection CreateBindingElements()
{
var result = base.CreateBindingElements();
var replacements = result.OfType<MessageEncodingBindingElement>().ToList();
foreach (var messageEncodingBindingElement in replacements)
{
var index = result.IndexOf(messageEncodingBindingElement);
result.Remove(messageEncodingBindingElement);
result.Insert(index, new MyMessageEncodingBindingElement(messageEncodingBindingElement));
}
return result;
}
}
public class MyMessageEncodingBindingElement : MessageEncodingBindingElement
{
private readonly MessageEncodingBindingElement _element;
public MyMessageEncodingBindingElement(MessageEncodingBindingElement element)
{
_element = element;
}
public override BindingElement Clone()
{
var result = _element.Clone();
if (result is MessageEncodingBindingElement)
return new MyMessageEncodingBindingElement(result as MessageEncodingBindingElement);
return result;
}
public override MessageEncoderFactory CreateMessageEncoderFactory()
{
return new MyMessageEncoderFactory(_element.CreateMessageEncoderFactory());
}
}
The method CreateMessageEncoderFactory() is never called even when the constructor and Clone method are hit when breakpoints are set. Any help? I'm trying to set a custom MessageEncoder and MessageEncoderFactory class to modify the instantiation process of the Message object.
You can use a WebContentTypeMapper for that. That's a property of the WebHttpBinding, and you can customize how the deserialization will be done by the encoder from that binding, including forcing it to always use the JSON deserializer, regardless of the incoming message's Content-Type. The code below shows how this can be done.
public class StackOverflow_13225272
{
[DataContract]
public class Person
{
[DataMember]
public string Name { get; set; }
[DataMember]
public int Age { get; set; }
public override string ToString()
{
return string.Format("Person[Name={0},Age={1}]", Name, Age);
}
}
[ServiceContract]
public interface ITest
{
[WebGet(ResponseFormat = WebMessageFormat.Json)]
Person GetPerson(string responseContentType);
}
public class Service : ITest
{
public Person GetPerson(string responseContentType)
{
WebOperationContext.Current.OutgoingResponse.ContentType = responseContentType;
return new Person { Name = "John Doe", Age = 29 };
}
}
class AllJsonContentTypeMapper : WebContentTypeMapper
{
public override WebContentFormat GetMessageFormatForContentType(string contentType)
{
return WebContentFormat.Json;
}
}
public static void Test()
{
string baseAddress = "http://" + Environment.MachineName + ":8000/Service";
WebServiceHost host = new WebServiceHost(typeof(Service), new Uri(baseAddress));
host.Open();
Console.WriteLine("Host opened");
#if USE_NETFX4
// This works on .NET 4.0 and beyond
WebHttpBinding binding = new WebHttpBinding();
binding.ContentTypeMapper = new AllJsonContentTypeMapper();
#else
// This works on .NET 3.5
CustomBinding binding = new CustomBinding(new WebHttpBinding());
binding.Elements.Find<WebMessageEncodingBindingElement>().ContentTypeMapper = new AllJsonContentTypeMapper();
ChannelFactory<ITest> factory = new ChannelFactory<ITest>(binding, new EndpointAddress(baseAddress));
#endif
ChannelFactory<ITest> factory = new ChannelFactory<ITest>(binding, new EndpointAddress(baseAddress));
factory.Endpoint.Behaviors.Add(new WebHttpBehavior());
ITest proxy = factory.CreateChannel();
Console.WriteLine("With JSON: {0}", proxy.GetPerson("application/json"));
Console.WriteLine("With XML: {0}", proxy.GetPerson("application/xml"));
Console.Write("Press ENTER to close the host");
Console.ReadLine();
host.Close();
}
}
This might work.
public class ForceJsonClientMessageFormatter : IClientMessageFormatter
{
private readonly DataContractJsonSerializer _jsonSerializer;
public ForceJsonClientMessageFormatter(Type responseType)
{
_jsonSerializer = new DataContractJsonSerializer(responseType);
}
public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)
{
throw new NotImplementedException("This client message formatter is for replies only!");
}
public object DeserializeReply(Message message, object[] parameters)
{
string messageBody = message.GetBody<string>();
using (MemoryStream messageStream = new MemoryStream(Encoding.UTF8.GetBytes(messageBody)))
{
messageStream.Seek(0, SeekOrigin.Begin);
object deserializedObject = _jsonSerializer.ReadObject(messageStream);
return deserializedObject;
}
}
}
public class ForceJsonWebHttpBehavior : WebHttpBehavior
{
protected override IClientMessageFormatter GetReplyClientFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
{
return new ForceJsonClientMessageFormatter(operationDescription.Messages[1].Body.ReturnValue.Type);
}
}
I haven't tried it, but I think this will work. You can create a custom IClientMessageFormatter which overwrites the message format to Json, wrap that in a behavior, and then apply that behavior to your client endpoint configuration.
public class ForceJsonClientMessageFormatterDecorator : IClientMessageFormatter
{
private readonly IClientMessageFormatter _decoratedFormatter;
public ForceJsonClientMessageFormatterDecorator(IClientMessageFormatter decoratedFormatter)
{
_decoratedFormatter = decoratedFormatter;
}
public object DeserializeReply(Message message, object[] parameters)
{
message.Properties[WebBodyFormatMessageProperty.Name] = new WebBodyFormatMessageProperty(WebContentFormat.Json);
return _decoratedFormatter.DeserializeReply(message, parameters);
}
public Message SerializeRequest(MessageVersion messageVersion, object[] parameters)
{
return _decoratedFormatter.SerializeRequest(messageVersion, parameters);
}
}
public class ForceJsonWebHttpBehavior : WebHttpBehavior
{
protected override IClientMessageFormatter GetReplyClientFormatter(OperationDescription operationDescription, ServiceEndpoint endpoint)
{
IClientMessageFormatter decoratedFormatter = base.GetReplyClientFormatter(operationDescription, endpoint);
return new ForceJsonClientMessageFormatterDecorator(decoratedFormatter);
}
}

Why does adding a constructor fail this MSpec test with System.InvalidOperationException?

I have this first version of a class
public class GenerateAuthorisationWorkflows : IGenerateAuthorisationWorkflows
{
public IList<Guid> FromDtaObjects(IList<DtaObject> dtaObjects, Employee requestingEmployee)
{
foreach (var dtaObject in dtaObjects) { }
return new List<Guid>();
}
public IList<Guid> FromDtaObjects()
{
return new List<Guid>();
}
}
And the MSpec tests for it
public abstract class specification_for_generate_workflows : Specification<GenerateAuthorisationWorkflows>
{
protected static IWorkflowService workflowService;
Establish context = () => { workflowService = DependencyOf<IWorkflowService>(); };
}
[Subject(typeof(GenerateAuthorisationWorkflows))]
public class when_generate_workflows_is_called_with_a_dta_object_list_and_an_employee : specification_for_generate_workflows
{
static IList<Guid> result;
static IList<DtaObject> dtaObjects;
static Employee requestingEmployee;
Establish context = () =>
{
var mocks = new MockRepository();
var stubDtaObject1 = mocks.Stub<DtaObject>();
var stubDtaObject2 = mocks.Stub<DtaObject>();
var dtaObjectEnum = new List<DtaObject>{stubDtaObject1,stubDtaObject2}.GetEnumerator();
dtaObjects = mocks.Stub<IList<DtaObject>>();
dtaObjects.Stub(x => x.GetEnumerator()).Return(dtaObjectEnum).WhenCalled(x => x.ReturnValue = dtaObjectEnum);
requestingEmployee = mocks.Stub<Employee>();
mocks.ReplayAll();
};
Because of = () => result = subject.FromDtaObjects(dtaObjects, requestingEmployee);
It should_enumerate_the_dta_objects = () => dtaObjects.received(x=> x.GetEnumerator());
It should_call_workflow_host_helper = () => workflowService.AssertWasCalled(x => x.StartWorkflow());
}
With this configuration, my first test passes and my second test fails, as expected. I added a constructor to the class to inject the IWorkflowService.
public class GenerateAuthorisationWorkflows : IGenerateAuthorisationWorkflows
{
IWorkflowService _workflowService;
GenerateAuthorisationWorkflows(IWorkflowService workflowService)
{
_workflowService = workflowService;
}
public IList<Guid> FromDtaObjects(IList<DtaObject> dtaObjects, Employee requestingEmployee)
{
foreach (var dtaObject in dtaObjects)
{
Guid workflowKey = _workflowService.StartWorkflow();
}
return new List<Guid>();
}
public IList<Guid> FromDtaObjects()
{
return new List<Guid>();
}
}
Now, when I run the tests, they fail at the Because:
System.InvalidOperationException: Sequence contains no elements
at System.Linq.Enumerable.First(IEnumerable`1 source)
at MSpecTests.EmployeeRequestSystem.Tasks.Workflows.when_generate_workflows_is_called_with_a_dta_object_list_and_an_employee.<.ctor>b__4() in GenerateAuthorisationWorkflowsSpecs.cs: line 76
For clarity, line 76 above is:
Because of = () => result = subject.FromDtaObjects(dtaObjects, requestingEmployee);
I've tried tracing the problem but am having no luck. I have tried setting up a constructor that takes no arguments but it raises the same error. I have similar classes with IoC dependencies that work fine using MSpec/Rhino Mocks, where am I going wrong?
Castle Windsor requires a public constructor to instantiate a class. Adding public to the constructor allows correct operation.
public class GenerateAuthorisationWorkflows : IGenerateAuthorisationWorkflows
{
IWorkflowService _workflowService;
public GenerateAuthorisationWorkflows(IWorkflowService workflowService)
{
_workflowService = workflowService;
}
public IList<Guid> FromDtaObjects(IList<DtaObject> dtaObjects, Employee requestingEmployee)
{
foreach (var dtaObject in dtaObjects)
{
Guid workflowKey = _workflowService.StartWorkflow();
}
return new List<Guid>();
}
public IList<Guid> FromDtaObjects()
{
return new List<Guid>();
}
}
Rowan, looks like you answered your own question. It's good practice to explicitly state the access modifiers! By default, C# chooses private. These kinds of errors are easy to miss!
I can also see that your Establish block is too complicated. You're testing the implementation details and not the behavior. For example, you are
stubbing the GetEnumerator call that's implicitly made inside the foreach loop.
asserting that the workflow service was called only once
mixing MSpec automocking and your own local mocks
You're not actually testing that you got a GUID for every object in the input list. If I were you, I'd test the behavior like this...
public class GenerateAuthorisationWorkflows : IGenerateAuthorisationWorkflows
{
private readonly IWorkflowService _service;
public GenerateAuthorisationWorkflows(IWorkflowService service)
{
_service = service;
}
public List<Guid> FromDtaObjects(List<DtaObject> input, Employee requestor)
{
// I assume that the workflow service generates a new key
// per input object. So, let's pretend the call looks like
// this. Also using some LINQ to avoid the foreach or
// building up a local list.
input.Select(x => _service.StartWorkflow(requestor, x)).ToList();
}
}
[Subject(typeof(GenerateAuthorisationWorkflows))]
public class When_generating_authorisation_keys_for_this_input
: Specification<GenerateAuthorisationWorkflows>
{
private static IWorkflowService _service;
private static Employee _requestor = new Employee();
private static List<DtaObject> _input = new List<DtaObject>()
{
new DtaObject(),
new DtaObject(),
};
private static List<Guid> _expected = new List<Guid>()
{
Guid.NewGuid(),
Guid.NewGuid(),
};
private static List<Guid> _actual = new List<Guid>();
Establish context = () =>
{
// LINQ that takes each item from each list in a pair. So
// the service is stubbed to return a specific GUID per
// input DtaObject.
_input.Zip(_expected, (input, output) =>
{
DependencyOf<IWorkflowService>().Stub(x => x.StartWorkflow(_requestor, input)).Return(output);
});
};
Because of = () => _actual = Subject.FromDtaObjects(_input, _requestor);
// This should be an MSpec collection assertion that
// ensures that the contents of the collections are
// equivalent
It should_get_a_unique_key_per_input = _actual.ShouldEqual(_expected);
}