Xamarin Forms with WorkManger - xamarin

I am trying to use the Xamarin implementation of WorkManager, in the nuget package Xamarin.Android.Arch.Work.Runtime.
The questions is: how to pass "complex" parameters to the worker class?
I have a Xamarin Forms application with DI and others classes, but the job only receives a Java.Lang.Object.
My code:
// Method to Schedule the Job
// See the dataParam? That line throw an exception
// I can manage to pass a simple string or int to the job this way, but not
// complex classes
public void ScheduleAppJobs(IContainerProvider containerRegistry)
{
//here the code throw an exception
var dataParam = new Data.Builder().Put("param", new JobParameter());
var syncWorkerReuest = PeriodicWorkRequest.Builder.From<SyncChecklistWorker>(TimeSpan.FromMinutes(5))
.SetInputData(dataParam.Build())
.Build();
WorkManager.Instance.Enqueue(syncWorkerReuest);
}
//this was my try to create a custom class and populate with my objects
//But didn't worked
public class JobParameter : Java.Lang.Object
{
}
//my job implementation
public class SyncChecklistWorker : Worker
{
public SyncChecklistWorker(Context context, WorkerParameters workerParameters) : base(context, workerParameters)
{
}
public override Result DoWork()
{
if (InputData.KeyValueMap.TryGetValue("param", out Java.Lang.Object #object))
{
var jobParam = (JobParameter)#object;
// here I would like to get my DI container to resolve services and execute business logic
// var diResolver = jobParam.GetDIContainer();
return Result.InvokeSuccess();
}
return Result.InvokeRetry();
}
}
}
The problem is:
The only way to pass input data to a jobs the Data.Builder only accepts Java.Lang.Object. Even trying to approach of having JobParameter : Java.Lang.Object I get the following error when trying to execute the: new Data.Builder().Put("param", new JobParameter()); Error: Java.Lang.IllegalArgumentException: 'Key param has invalid type class crc648d221dddf00bc7fb.JobParameter'
On the official Microsoft Docs the FireBase Job Dispatcher nuget is deprecated. So how to work with the new WorkManager one?
FireBase Job Dispatcher Doc:
https://learn.microsoft.com/en-us/xamarin/android/platform/firebase-job-dispatcher
Deprecated nuget:
https://www.nuget.org/packages/Xamarin.Firebase.JobDispatcher
Any idea of how to solve this?

Work request from Data builder accepts only the premitive types. You can pass the object by serializing to JSON string format and in DoWork() you can deserialize it.
public void ScheduleAppJobs(IContainerProvider containerRegistry)
{
var dataParam = new Data.Builder().PutString("param",serializeToJson(new
MyClass()));
var syncWorkerReuest = PeriodicWorkRequest.Builder.From<SyncChecklistWorker>
(TimeSpan.FromMinutes(5))
.SetInputData(dataParam.Build())
.Build();
WorkManager.Instance.Enqueue(syncWorkerReuest);
}
public override Result DoWork()
{
var jsonString = InputData.GetString("param");
var myClassObj = deserializeFromJson(jsonString );
}
using Newtonsoft.Json;
public string serializeToJson(MyClass myClassObj)
{
var resultString = JsonConvert.SerializeObject(myClassObj);
return resultString;
}
// Deserialize to single object.
public MyClass deserializeFromJson(string jsonString)
{
var serializer = new JsonSerializer();
var resultObject = serializer.Deserialize<MyClass>(jsonString);
}

Related

Argument 1: cannot convert from 'Callbacks.OnConsoleLogHandler' to 'Kotlin.Jvm.Functions.IFunction2'

I created a binding library, via a .arr file.
And in one of the methods I need to pass a callback.
What I was trying to do:
this is my delegate:
public class Callbacks
{
public delegate void OnConsoleLogHandler(string message, Sometype type);
}
this is my Configuration class:
public class Configuration
{
public Callbacks.OnConsoleLogHandler onConsoleLog;
public Configuration()
{
}
public Configuration(Action<Configuration> action)
{
var config = new Configuration();
action?.Invoke(config);
onConsoleLog = config.onConsoleLog;
}
}
here's how I'm trying to call a native method:
public static void Start(Configuration configuration)
{
var onConsoleLog = configuration.onConsoleLog;
Package.Name.Configuration config = new Package.Name.Configuration();
config.SetOnConsoleLog(onConsoleLog);
}
But on this row config.SetOnConsoleLog(onConsoleLog);
I get error:
Argument 1: cannot convert from 'Callbacks.OnConsoleLogHandler' to 'Kotlin.Jvm.Functions.IFunction2'
If I try something like this:
config.SetOnConsoleLog(() => { });
I get:
Cannot convert lambda expression to type 'IFunction2' because it is not a delegate type
How can I put the delegate as a callback? Any suggestions?

Access IStatePropertyAccessor botstate in a controller in v4

If you need to access the state in botframework v4 outside of a bot context (that does not implement IBot), for instance a Controller, how could you easily get a hold of this state object? The problem is that you can't really inject it directly because it needs to be initialized with a ChannelId and ConversationId.
The following approach works but looks a bit strange as I am using the TestAdapter to initialize the TurnContext. Isn't there a better, more obvious method to get a hold of the state?
public class SampleController : ControllerBase
{
private readonly MyBotAccessors _botAccessors;
public SampleController(MyBotAccessors botAccessors)
{
_botAccessors = botAccessors;
}
}
public async Task Reset(string conversationKey, string channelId)
{
var turnContext = new TurnContext(new TestAdapter(), new Activity { ChannelId = channelId, Conversation = new ConversationAccount { Id = conversationKey } });
await _botAccessors.MyContextState.SetAsync(turnContext, new MyContextState().Reset());
await _botAccessors.ConversationState.SaveChangesAsync(turnContext);
}

Adding custom data for an operation to Application Insights telemetry

I'm trying to add a bunch of custom data fields to every piece of telemetry I can, and this data is consistent across a single operation, but varies from operation to operation.
I have a custom ITelemetryInitializer, and within that I can do something like:
public class MyInitializer : ITelemetryInitializer
{
public void Initialize(Microsoft.ApplicationInsights.Channel.ITelemetry telemetry)
{
telemetry.Context.Properties[ "platform" ] = "PC";
}
}
But I don't understand how I'm suppose to push this data into this initializer.
I've added something like this:
public class MyInitializer : ITelemetryInitializer
{
private string mPlatform = "unknown";
public void Initialize(Microsoft.ApplicationInsights.Channel.ITelemetry telemetry)
{
telemetry.Context.Properties[ "platform" ] = mPlatform;
}
public void SetPlatform(string platform)
{
mPlatform = platform
}
}
And then at the controller level I do something like this:
foreach (var init in TelemetryConfiguration.Active.TelemetryInitializers)
{
var customInit = init as MyInitializer;
if (customInit != null)
{
customInit.SetPlatform(requestPlatform);
}
}
But this is horribly clunky, and prone to error (e.g. if a piece of telemetry gets sent before this function is called), and I'm not really sure if this is thread-safe.
What's the intended way of passing around this kind of data?
I think I've solved this now, the solution is to write to the properties of the TelemetryClient within the controller like this:
[Route( "[controller]" )]
public class MyController : Controller
{
private readonly TelemetryClient mTelemetryClient;
public MyController(
TelemetryClient TelemetryClientArg )
{
mTelemetryClient = TelemetryClientArg;
mTelemetryClient.Context.Properties.Remove("platform");
}
[HttpPost]
[Produces( "application/json" )]
public IActionResult Post( [FromBody] RequestClass RequestData )
{
mTelemetryClient.TrackTrace("Test trace 1"); // doesn't have platform set
mTelemetryClient.Context.Properties["platform"] = RequestData.platform;
mTelemetryClient.TrackTrace("Test trace 2"); // has platform set correctly
}
}
This seems to be safe as the controller constructor appears to be called before each http request is processed and the context within the TelemetryClient is unique per thread. I would like to get confirmation from the team that this is reasonable.

Get a specific service implementation based on a parameter

In my Sling app I have data presenting documents, with pages, and content nodes. We mostly server those documents as HTML, but now I would like to have a servlet to serve these documents as PDF and PPT.
Basically, I thought about implementing the factory pattern : in my servlet, dependending on the extension of the request (pdf or ppt), I would get from a DocumentBuilderFactory, the proper DocumentBuilder implementation, either PdfDocumentBuilder or PptDocumentBuilder.
So first I had this:
public class PlanExportBuilderFactory {
public PlanExportBuilder getBuilder(String type) {
PlanExportBuilder builder = null;
switch (type) {
case "pdf":
builder = new PdfPlanExportBuilder();
break;
default:
logger.error("Unsupported plan export builder, type: " + type);
}
return builder;
}
}
In the servlet:
#Component(metatype = false)
#Service(Servlet.class)
#Properties({
#Property(name = "sling.servlet.resourceTypes", value = "myApp/document"),
#Property(name = "sling.servlet.extensions", value = { "ppt", "pdf" }),
#Property(name = "sling.servlet.methods", value = "GET")
})
public class PlanExportServlet extends SlingSafeMethodsServlet {
#Reference
PlanExportBuilderFactory builderFactory;
#Override
protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
Resource resource = request.getResource();
PlanExportBuilder builder = builderFactory.getBuilder(request.getRequestPathInfo().getExtension());
}
}
But the problem is that in the builder I would like to reference other services to access Sling resources, and with this solution, they're not bound.
I looked at Services Factory with OSGi but from what I've understood, you use them to configure differently the same implementation of a service.
Then I found that you can get a specific implementation by naming it, or use a property and a filter.
So I've ended up with this:
public class PlanExportBuilderFactory {
#Reference(target = "(builderType=pdf)")
PlanExportBuilder pdfPlanExportBuilder;
public PlanExportBuilder getBuilder(String type) {
PlanExportBuilder builder = null;
switch (type) {
case "pdf":
return pdfPlanExportBuilder;
default:
logger.error("Unsupported plan export builder, type: " + type);
}
return builder;
}
}
The builder defining a "builderType" property :
// AbstractPlanExportBuilder implements PlanExportBuilder interface
#Component
#Service(value=PlanExportBuilder.class)
public class PdfPlanExportBuilder extends AbstractPlanExportBuilder {
#Property(name="builderType", value="pdf")
public PdfPlanExportBuilder() {
planDocument = new PdfPlanDocument();
}
}
I would like to know if it's a good way to retrieve my PDF builder implementation regarding OSGi good practices.
EDIT 1
From Peter's answer I've tried to add multiple references but with Felix it doesn't seem to work:
#Reference(name = "planExportBuilder", cardinality = ReferenceCardinality.MANDATORY_MULTIPLE, policy = ReferencePolicy.DYNAMIC)
private Map<String, PlanExportBuilder> builders = new ConcurrentHashMap<String, PlanExportBuilder>();
protected final void bindPlanExportBuilder(PlanExportBuilder b, Map<String, Object> props) {
final String type = PropertiesUtil.toString(props.get("type"), null);
if (type != null) {
this.builders.put((String) props.get("type"), b);
}
}
protected final void unbindPlanExportBuilder(final PlanExportBuilder b, Map<String, Object> props) {
final String type = PropertiesUtil.toString(props.get("type"), null);
if (type != null) {
this.builders.remove(type);
}
}
I get these errors :
#Reference(builders) : Missing method bind for reference planExportBuilder
#Reference(builders) : Something went wrong: false - true - MANDATORY_MULTIPLE
#Reference(builders) : Missing method unbind for reference planExportBuilder
The Felix documentation here http://felix.apache.org/documentation/subprojects/apache-felix-maven-scr-plugin/scr-annotations.html#reference says for the bind method:
The default value is the name created by appending the reference name to the string bind. The method must be declared public or protected and take single argument which is declared with the service interface type
So according to this, I understand it cannot work with Felix, as I'm trying to pass two arguments. However, I found an example here that seems to match what you've suggested but I cannot make it work: https://github.com/Adobe-Consulting-Services/acs-aem-samples/blob/master/bundle/src/main/java/com/adobe/acs/samples/services/impl/SampleMultiReferenceServiceImpl.java
EDIT 2
Just had to move the reference above the class to make it work:
#References({
#Reference(
name = "planExportBuilder",
referenceInterface = PlanExportBuilder.class,
policy = ReferencePolicy.DYNAMIC,
cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE)
})
public class PlanExportServlet extends SlingSafeMethodsServlet {
Factories are evil :-) Main reason is of course the yucky class loading hacks that are usually used but also because they tend to have global knowledge. In general, you want to be able to add a bundle with a new DocumentBuilder and then that type should become available.
A more OSGi oriented solution is therefore to use service properties. This could look like:
#Component( property=HTTP_WHITEBOARD_FILTER_REGEX+"=/as")
public class DocumentServlet {
final Map<String,DocBuilder> builders = new ConcurrentHashMap<>();
public void doGet( HttpServletRequest rq, HttpServletResponse rsp )
throws IOException, ServletException {
InputStream in = getInputStream( rq.getPathInfo() );
if ( in == null )
....
String type = toType( rq.getPathInfo(), rq.getParameter("type") );
DocBuilder docbuilder = builders.get( type );
if ( docbuilder == null)
....
docbuilder.convert( type, in, rsp.getOutputStream() );
}
#Reference( cardinality=MULTIPLE, policy=DYNAMIC )
void addDocBuilder( DocBuilder db, Map<String,Object> props ) {
docbuilders.put(props.get("type"), db );
}
void removeDocBuilder(Map<String,Object> props ) {
docbuilders.remove(props.get("type"));
}
}
A DocBuilder could look like:
#Component( property = "type=ppt-pdf" )
public class PowerPointToPdf implements DocBuilder {
...
}

running wf workflow from mvc controller

enter code hereI intend to use WF4.5 in a web application which is written by MVC Framework. I have used WorkflowApplication class instance to run my WorkFlow with. but whenever i call the method in controller that run the instance I get this error:
An asynchronous operation cannot be started at this time. Asynchronous operations may only be started within an asynchronous handler or module or during certain events in the Page lifecycle. If this exception occurred while executing a Page, ensure that the Page is marked <%# Page Async="true" %>. This exception may also indicate an attempt to call an "async void" method, which is generally unsupported within ASP.NET request processing. Instead, the asynchronous method should return a Task, and the caller should await it
I have written this class which is resposnsible to execute workflow:
public class WorkFlowsPipeline : IWorkFlowsPipeline
{
private IUnitOfWork _unitOfWork;
private SqlWorkflowInstanceStore _instanceStore;
public WorkFlowsPipeline(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
//workflowInstanceStore
_instanceStore = new SqlWorkflowInstanceStore();
_instanceStore.ConnectionString ="data source=.;initial catalog=WFPersist;user id=sa;password=1;";
}
public void RecordPersistedInstanceForTheUser(int userId,Guid instanceId, Models.Enums.WorkFlowTypeEnum workFlowType)
{
_unitOfWork.UsersWorkFlows.Add(new UsersWorkFlowsInstance
{
UserId = userId,
WorkFlowId=instanceId,
WorkFlowType = workFlowType
});
}
public void RunCompleteProfileForUser(int userId)
{
var usersWorkFlow = _unitOfWork.UsersWorkFlows.GetAll().FirstOrDefault(x => x.UserId == userId);
if (usersWorkFlow == null)
{
Activity rentalWorkflow = new Activity1();
Dictionary<string, object> wfArg = new Dictionary<string, object>()
{
{
"UOW", _unitOfWork
},
{
"UserId",userId
}
};
var _wfApp = new WorkflowApplication(rentalWorkflow, wfArg);
_wfApp.SynchronizationContext = SynchronizationContext.Current;
_wfApp.InstanceStore = _instanceStore;
//_wfApp.Extensions.Add(this);
var instanceId=_wfApp.Id;
_wfApp.Run();
RecordPersistedInstanceForTheUser(userId, instanceId,WorkFlowTypeEnum.CompleteProfile);
}
else
{
//get id of instance load it from database and run it
}
}
}
and I called the method in my controller action in this way:
public async Task<ActionResult> Index()
{
var userId = User.Identity.GetUserId<int>();
_workFlowsPipeline.RunCompleteProfileForUser(userId);
return View();
}
Use WorkflowInvoker instead of WorkflowApplication.

Resources