SignalR KnockoutJS and AzureTodo OnChangeEvent - azure

I would like CollectionChanged triggered when todo items is inserted in my table for Azure from elsewhere.
I have a mobileapp where i can insert items via TodoItemManager.
I have a website where items is listed.
Now i would like without refreshing the site, items should get added.
One option is to have a setInterval but i dont like that option.
<div class="products">
<div class="row" data-bind="template: { name: 'productTemplate', foreach: products }">
</div>
<span class="messageClass" style="color: red;"></span>
</div>
<script type="text/html" id="productTemplate">
<div class="col-sm-6 col-md-4">
<div class="thumbnail">
<div class="caption">
<h3 data-bind="text: name"></h3>
</div>
</div>
</div>
</script>
<script>
$(function () {
//debugger;
function productViewModel(id, name) {
this.productId = id;
this.name = ko.observable(name);
var self = this;
}
function productListViewModel() {
//debugger;
this.hub = $.connection.myHub;
this.products = ko.observableArray([]);
var products = this.products;
this.init = function () {
this.hub.server.getAllProducts();
}
this.hub.client.getAllProducts = function (allProducts) {
//debugger;
var mappedProducts = $.map(allProducts, function (item) {
//debugger;
return new productViewModel(item.ProductId, item.Name)
});
products(mappedProducts);
}
}
var vm = new productListViewModel();
ko.applyBindings(vm);
$.connection.hub.start(function () {
vm.init();
}).done(function () {
});;
});
</script>
Another option is to trigger method GetAllProducts in my Hub from the app so it will trigger
await Clients.All.getAllProducts(vm.Products.ToArray());
But i dont like that approach.
I would like to know how i can update my data that is listed on my webpage when i add items to azure table without refreshing the browser.
Here is my hub
public class MyHub : Hub
{
private ObservableCollection<TodoItem> persons;
public MyHub()
{
}
public async Task GetAllProducts()
{
var data = await GetLogs();
VoteViewModel vm = new VoteViewModel();
vm.Products = data.Select(m => new Products() { Name = m.Name }).ToList();
//vm.Products = new List<Products>() { new Products() { Name = "Sample", ProductId = 1 } };
await Clients.All.getAllProducts(vm.Products.ToArray());
}
private async Task<List<TodoItem>> GetLogs()
{
persons = await TodoItemManager.DefaultManager.GetTodoItemsAsync();
persons.CollectionChanged += this.OnCollectionChanged;
return persons.ToList();
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
}
class VoteViewModel
{
public List<Products> Products { get; set; }
}
public class Products
{
public int ProductId { get; set; }
public string Name { get; set; }
}
}
And a look in TodoItemManager:
public partial class TodoItemManager
{
static TodoItemManager defaultInstance = new TodoItemManager();
MobileServiceClient client;
#if OFFLINE_SYNC_ENABLED
IMobileServiceSyncTable<TodoItem> todoTable;
#else
IMobileServiceTable<TodoItem> todoTable;
#endif
const string offlineDbPath = #"localstore.db";
private TodoItemManager()
{
this.client = new MobileServiceClient(AzureSettings.ApplicationURL);
#if OFFLINE_SYNC_ENABLED
var store = new MobileServiceSQLiteStore(offlineDbPath);
store.DefineTable<TodoItem>();
//Initializes the SyncContext using the default IMobileServiceSyncHandler.
this.client.SyncContext.InitializeAsync(store);
this.todoTable = client.GetSyncTable<TodoItem>();
#else
this.todoTable = client.GetTable<TodoItem>();
#endif
}
public static TodoItemManager DefaultManager
{
get
{
return defaultInstance;
}
private set
{
defaultInstance = value;
}
}
public MobileServiceClient CurrentClient
{
get { return client; }
}
public bool IsOfflineEnabled
{
get { return todoTable is Microsoft.WindowsAzure.MobileServices.Sync.IMobileServiceSyncTable<TodoItem>; }
}
public async Task<ObservableCollection<TodoItem>> GetTodoItemsAsync(bool syncItems = false)
{
try
{
#if OFFLINE_SYNC_ENABLED
if (syncItems)
{
await this.SyncAsync();
}
#endif
IEnumerable<TodoItem> items = await todoTable
.Where(todoItem => !todoItem.Done)
.ToEnumerableAsync();
return new ObservableCollection<TodoItem>(items);
}
catch (MobileServiceInvalidOperationException msioe)
{
Debug.WriteLine(#"Invalid sync operation: {0}", msioe.Message);
}
catch (Exception e)
{
Debug.WriteLine(#"Sync error: {0}", e.Message);
}
return null;
}
public async Task SaveTaskAsync(TodoItem item)
{
if (item.Id == null)
{
await todoTable.InsertAsync(item);
}
else
{
await todoTable.UpdateAsync(item);
}
}
#if OFFLINE_SYNC_ENABLED
public async Task SyncAsync()
{
ReadOnlyCollection<MobileServiceTableOperationError> syncErrors = null;
try
{
await this.client.SyncContext.PushAsync();
await this.todoTable.PullAsync(
//The first parameter is a query name that is used internally by the client SDK to implement incremental sync.
//Use a different query name for each unique query in your program
"allTodoItems",
this.todoTable.CreateQuery());
}
catch (MobileServicePushFailedException exc)
{
if (exc.PushResult != null)
{
syncErrors = exc.PushResult.Errors;
}
}
// Simple error/conflict handling. A real application would handle the various errors like network conditions,
// server conflicts and others via the IMobileServiceSyncHandler.
if (syncErrors != null)
{
foreach (var error in syncErrors)
{
if (error.OperationKind == MobileServiceTableOperationKind.Update && error.Result != null)
{
//Update failed, reverting to server's copy.
await error.CancelAndUpdateItemAsync(error.Result);
}
else
{
// Discard local change.
await error.CancelAndDiscardItemAsync();
}
Debug.WriteLine(#"Error executing sync operation. Item: {0} ({1}). Operation discarded.", error.TableName, error.Item["id"]);
}
}
}
#endif
}
As you can se in my hub i tried with
this.OnCollectionChanged
But this will change only if
private ObservableCollection<TodoItem> persons;
changes and this will not happen so i need to invoke
GetTodoItemsAsync
again to fetch the data.
How do i update my list on client when new items is added to azure table without refreshing the whole site?
Hope you understand where im aiming at.
BR

Another option would be to set a interval on server instead of client.
Like:
public class MyHub : Hub
{
private ObservableCollection<TodoItem> persons;
public MyHub()
{
Intervalla();
}
public async Task Intervalla()
{
EasyTimer.SetInterval(async () =>
{
var data = await GetLogs();
VoteViewModel vm = new VoteViewModel();
vm.Products = data.Select(m => new Products() { Name = m.Name }).ToList();
//vm.Products = new List<Products>() { new Products() { Name = "Sample", ProductId = 1 } };
await Clients.All.getAllProducts(vm.Products.ToArray());
}, 1000);
}
Could this be a smart/er option?

Related

Unable to find WebHook filters for the 'xx' receiver. Add the required configuration by calling a receiver method that calls ''AddWebHooks'

I am implementing webhook using asp.net core 3.1 webhook package. This is a custom webhook poc and I need to expose this webhook to external users. During runtime I am facing below error and unable to solve it.
What can I try next?
Error:
Unable to find WebHook filters for the 'jr4o27tr2r472' receiver. Add the required configuration by calling a receiver-specific method that calls 'Microsoft.Extensions.DependencyInjection.IMvcBuilder.AddWebHooks' or 'IMvcCoreBuilder.AddWebHooks' in the application startup code. For example, call 'IMvcCoreBuilder.AddGitHubWebHooks' to configure a minimal GitHub receiver.
When I hit this url (http://localhost:49846/api/webhooks/incoming/jr4o27tr2r472/teleported), I am getting this issue in eventviewer.
Note: I have added required webhook services as part of configurationservice method.
public static class UnicornServiceCollectionSetup
{
public static void AddUnicornServices(IServiceCollection services)
{
WebHookMetadata.Register<UnicornMetadata>(services);
services.AddSingleton<UnicornSignatureFilter>();
}
}
public static class UnicornMvcCoreBuilderExtensions
{
public static IMvcCoreBuilder AddUnicornWebHooks(this IMvcCoreBuilder builder)
{
UnicornServiceCollectionSetup.AddUnicornServices(builder.Services);
return builder.AddWebHooks();
}
}
public class UnicornMetadata : WebHookMetadata, IWebHookFilterMetadata
{
private readonly UnicornSignatureFilter _verifySignatureFilter;
public UnicornMetadata(UnicornSignatureFilter verifySignatureFilter)
: base(UnicornConstants.ReceiverName)
{
_verifySignatureFilter = verifySignatureFilter;
}
public override WebHookBodyType BodyType => WebHookBodyType.Json;
public void AddFilters(WebHookFilterMetadataContext context)
{
context.Results.Add(_verifySignatureFilter);
}
}
public class UnicornSignatureFilter : WebHookVerifySignatureFilter,
IAsyncResourceFilter
{
private readonly byte[] _secret;
public UnicornSignatureFilter(//IOptions<UnicornConfig> options,
IConfiguration configuration,
IHostingEnvironment hostingEnvironment,
ILoggerFactory loggerFactory)
: base(configuration, hostingEnvironment, loggerFactory)
{
//_secret = Encoding.UTF8.GetBytes(options.Value.SharedSecret);
_secret = Encoding.UTF8.GetBytes("secret");
}
public override string ReceiverName => UnicornConstants.ReceiverName;
public async Task OnResourceExecutionAsync(ResourceExecutingContext context,
ResourceExecutionDelegate next)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (next == null) throw new ArgumentNullException(nameof(next));
var request = context.HttpContext.Request;
if (!HttpMethods.IsPost(request.Method))
{
await next();
return;
}
var errorResult = EnsureSecureConnection(ReceiverName, request);
if (errorResult != null)
{
context.Result = errorResult;
return;
}
var header = GetRequestHeader(request,
UnicornConstants.SignatureHeaderName,
out errorResult);
if (errorResult != null)
{
context.Result = errorResult;
return;
}
byte[] payload;
using (var ms = new MemoryStream())
{
HttpRequestRewindExtensions.EnableBuffering(request);
await request.Body.CopyToAsync(ms);
payload = ms.ToArray();
request.Body.Position = 0;
}
if (payload == null || payload.Length == 0)
{
context.Result = new BadRequestObjectResult("No payload");
return;
}
var digest = FromBase64(header, UnicornConstants.SignatureHeaderName);
var secretPlusJson = _secret.Concat(payload).ToArray();
using (var sha512 = new SHA512Managed())
{
if (!SecretEqual(sha512.ComputeHash(secretPlusJson), digest))
{
context.Result =
new BadRequestObjectResult("Signature verification failed");
return;
}
}
await next();
}
}
Note: I am attaching source code in this webhookpoc.

Sending message to IoTHub fails

I have been working on a device which is sending some data to an Azure IoT hub
The device is doing this on two different locations in the code. On one side it works perfectly and I can connect to the Hub via Connection String and transport type MQTT_WebSocket_Only.
public static class Mqtt2IoTNew
{
private static string _DeviceConnectionString = Properties.Settings.Default.MqttUri;
private static TransportType _TransportType = TransportType.Mqtt_WebSocket_Only;
public static void Send(object argEntry, bool argIsList)
{
var deviceClient = DeviceClient.CreateFromConnectionString(_DeviceConnectionString, _TransportType);
deviceClient.ReceiveAsync(TimeSpan.FromSeconds(2)).Wait();
var message = new Message(deviceClient, argEntry, argIsList);
message.RunAsync().GetAwaiter().GetResult();
}
}
internal class Message
{
private DeviceClient _DeviceClient;
private readonly string _Message;
public Message(DeviceClient argDeviceClient, object argEntry, bool isList)
{
_DeviceClient = argDeviceClient;
StringBuilder stb = new StringBuilder();
if (isList)
{
foreach (var entity in (List<object>) argEntry)
{
stb.Append("<entity>").Append(JsonConvert.SerializeObject(entity)).Append("</entity>\n");
}
}
else
{
stb.Append(JsonConvert.SerializeObject(argEntry));
}
_Message = stb.ToString();
}
public async Task RunAsync()
{
await SendEvent().ConfigureAwait(false);
}
private async Task SendEvent()
{
Microsoft.Azure.Devices.Client.Message eventMessage = new Microsoft.Azure.Devices.Client.Message(Encoding.UTF8.GetBytes(_Message));
await _DeviceClient.SendEventAsync(eventMessage).ConfigureAwait(false);
}
}
//Call of method that does not work
protected override void DoOnCompleted(IRepository argRepository)
{
if (_CurrentlySendingTreadId.HasValue)
{
if (_CurrentlySendingTreadId.Value == Thread.CurrentThread.ManagedThreadId)
{
return;
}
}
TaskFactoryProvider.GetFactory().StartNew(()=>SendBatchProtocols());
}
public bool SendBatchProtocols()
{
using (var repository = RepositoryProviderHolder.RepositoryProvider.GetRepository(Constants.CONTAINERCONTRACT_PRODUCTIONREPOSITORY))
{
IQueryable<BatchProtocol> batchProtocolQuery = repository.GetQuery<BatchProtocol>().OrderBy(bp => bp.InternalNoInteger);
batchProtocolQuery = batchProtocolQuery.Where(bp => !bp.IsArchived).Take(1);
if (!batchProtocolQuery.Any()) return false;
var batchProtocols = batchProtocolQuery.ToList();
IsBatchProtocolSend = false;
try
{
foreach (var bps in batchProtocols)
{
Mqtt2IoTNew.Send(bps,false);
}
IsBatchProtocolSend = true;
}
catch (Exception ex)
{
throw;
}
}
return IsBatchProtocolSend;
}
//Call of Method that does work
private void AddEntitiesAndSaveChanges(IEnumerable argEntities)
{
if (argEntities == null)
{
return;
}
lock (_UnderlyingRepositoryAccessLockObject)
{
#region Log2DornerIoT
if (Properties.Settings.Default.Log2DornerIoT)
{
List<object> entities = new List<object>();
int i = 0;
foreach (var entity in argEntities)
{
if (i < 100)
{
entities.Add(entity);
i++;
}
else
{
try
{
Mqtt2IoTNew.Send(entities, true);
}
catch (Exception e)
{
throw;
}
entities.Clear();
i = 0;
}
}
}
}
on the other part of the code, I am only colling the same class to use to send method in the same way but here I get an exception which says "TLS authentication error" and the inner exception "Unable to connect to the remote server", "The underlying connection was closed: Could not establish trust relationship for the SSL/TLS secure channel".
But: I never used any kind of authorization not in the first part which works perfectly neither in the second.
I would be very happy if someone could help me. I have found nothing so fare regarding this issue.
Thanks for your time.
Michael
I found the reason why it didn't work. There was a Persmissice Certificate Policy applied that blocked the certificate at one side of the project. I disabled it and now it works perfectly fine.
Thanks for the help anyway.

Passing a list to partialview, BeginCollectionItem()

I want to pass a list to PartialView that has BeginCollectionItem(). Here is the code,
InquiryOrderViewModel
public class InquiryOrderViewModel
{
public InquiryOrder InquiryOrder { get; set; }
public List<InquiryOrderDetail> InquiryOrderDetails { get; set; }
public List<InquiryComponentDetail> InquiryComponentDetails { get; set; }
}
InquiryComponentDetail model
public class InquiryComponentDetail
{
[Key]
public int InquiryComponentDetailId { get; set; }
public int DesignCodeId { get; set; }
public int QualityReferenceId { get; set; }
public int Height { get; set; }
public int Length { get; set; }
public int GscmComp { get; set; }
public int Wastage { get; set; }
public int TotalYarn { get; set; }
public virtual DesignCodeQltyRef DesignCodeQltyRef { get; set; }
}
InquiryOrderIndex View and the Script to render multiple items at once
#model eKnittingData.InquiryOrderViewModel
#using (Html.BeginForm("Save", "InquiryOrder"))
{
..........
<div id="cmpDts">
#foreach (var item in Model.InquiryComponentDetails)
{
}
</div>
..........
}
<script>
var prev;
$(document).on('focus', '.class03', function () {
prev = $(this).val();
}).on('change', '.class03', function () {
if (prev != "") {
$.ajax({
url: '#Url.Action("ComponentDts", "InquiryOrder")', // dont hard code your url's
type: "GET",
data: { DesignCdId: $(this).val() }, // pass the selected value
success: function (data) {
$('.cmpCls').last().replaceWith(data);
}
});
}
else {
$.ajax({
url: '#Url.Action("ComponentDts", "InquiryOrder")', // dont hard code your url's
type: "GET",
data: { DesignCdId: $(this).val() }, // pass the selected value
success: function (data) {
$(".class03 option[value='']").remove();
$('#cmpDts').append(data);
}
});
}
});
</script>
The _DetailEditorRow PartialView which gives ddls with class03 and in main view where it got appended.(This is just to show you what is class03)
#model eKnittingData.InquiryOrderDetail
#using eKnitting.Helpers
#using (Html.BeginCollectionItem("InquiryOrderDetails"))
{
<div class="editorRow">
#Html.DropDownListFor(a => a.ComponentId, (SelectList)ViewBag.CompList, "Select", new { Class = "class02" })
#Html.DropDownListFor(a => a.DesignCodeId, (SelectList)ViewBag.DCodeList, "Select", new { Class = "class03" })
#Html.TextBoxFor(a => a.NoOfParts, new { Class = "class01" })
delete
</div>
}
and in main view it got appended to
<div id="editorRows">
#foreach (var item in Model.InquiryOrderDetails)
{
Html.RenderPartial("_DetailEditorRow", item);
}
</div>
_ComponentDetails PartialView to render items(a list has been passed at once)
#model List<eKnittingData.InquiryComponentDetail>
#using eKnitting.Helpers
<div class="cmpCls">
#foreach(var icd in Model)
{
using (Html.BeginCollectionItem("InquiryComponentDetails"))
{
<div class="innerCmpCls">
#Html.DisplayFor(a => icd.DesignCodeId)
#Html.DisplayFor(a => icd.QualityReferenceId)
#Html.TextBoxFor(a => icd.Height, new { Class="clsHeight clsSameHL"})
#Html.TextBoxFor(a => icd.Length, new { Class = "clsLength clsSameHL" })
#Html.TextBoxFor(a => icd.GscmComp, new { Class = "clsGscmComp clsSameHL" })
#Html.TextBoxFor(A => icd.Wastage, new { Class = "clsWastage" })
#Html.ActionLink("Fds", "View", new { id = icd.QualityReferenceId }, new { #class = "myLink", data_id = icd.QualityReferenceId })
#Html.TextBoxFor(a => icd.TotalYarn, new { Class = "clsTotalYarn" })
<br>
<div class="popFds"></div>
</div>
}
}
</div>
ActionResult that Passes a list at once and returns the PartialView
public ActionResult ComponentDts(int DesignCdId)
{
var objContext = new KnittingdbContext();
var QltyRefList = objContext.DesignCodeQltyRefs.Where(a=>a.DesignCodeId==DesignCdId).ToList();
var iocdList = new List<InquiryComponentDetail>();
foreach(DesignCodeQltyRef dcqr in QltyRefList)
{
iocdList.Add(new InquiryComponentDetail {
DesignCodeId=dcqr.DesignCodeId,
QualityReferenceId=dcqr.QltyRefId
});
}
return PartialView("_ComponentDetails", iocdList);
}
ActionResult for GET
var objContext = new KnittingdbContext();
var newIovm = new InquiryOrderViewModel();
var newIo = new InquiryOrder();
var iocdL = new List<InquiryComponentDetail>();
newIovm.InquiryOrder = newIo;
newIovm.InquiryComponentDetails = iocdL;
return View(newIovm);
ActionResult for POST
public ActionResult Save(InquiryOrderViewModel inquiryOrderViewModel)
{
.........
}
When user selects an item from a dropdownlist(class03), the items related to that item are rendered to the view using the PartialView(_ComponentDetails') and get appended. Then user selects another item from another ddl(class03), the related items are rendered and appended after earlier appended ones. User can go on like this.
Rendering and appending items works fine. But for the PostBack even though i get the number of items in the list correctly(I checked it by putting a break point on POST ActionResult ) all items content show null values. Pls guide me in the correct way for achieving this. All help appreciated. Thanks!
Your _ComponentDetails view is generating form controls that have name attributes that look like (where ### is a Guid)
name="InquiryComponentDetail[###].icd.Height"
which does not match your model because typeof InquiryComponentDetail does not contain a property named icd. In order to bind to your model, your name attribute would need
name="InquiryComponentDetail[###].Height"
To generate the correct html, you will need 2 partials
_ComponentDetailsList.cshtml (this will be called by the ComponentDts() method using return PartialView("_ComponentDetailsList", iocdList);)
#model List<eKnittingData.InquiryComponentDetail>
<div class="cmpCls">
#foreach(var item in Model)
{
Html.RenderPartial("_ComponentDetails", item);
}
</div>
_ComponentDetails.cshtml
#model eKnittingData.InquiryComponentDetail
using (Html.BeginCollectionItem("InquiryComponentDetails"))
{
<div class="innerCmpCls">
#Html.DisplayFor(a => a.DesignCodeId)
#Html.DisplayFor(a => a.QualityReferenceId)
#Html.TextBoxFor(a => a.Height, new { #class="clsHeight clsSameHL"}) // use #class, not Class
#Html.TextBoxFor(a => a.Length, new { Class = "clsLength clsSameHL" })
....
</div>
}

Azure notification hub tags not creating nor updating - to target specific user

Hi I am working on web api as back-end service where I am using Azure notification hub. I need to notify logged in user according to conditional business logic, in short target specific user. I extract code from this article. Everything works fine but tags is not creating nor updating. I need help. Here is my code snippet
// It returns registrationId
public async Task<OperationResult<string>> GetRegistrationIdAsync(string handle)
{
........
}
// actual device registration and tag update
public async Task<OperationResult<RegistrationOutput>> RegisterDeviceAsync(string userName, string registrationId, Platforms platform, string handle)
{
...........
registration.Tags.Add(string.Format("username : {0}", userName)); // THIS TAG IS NOT CREATING
await _hub.CreateOrUpdateRegistrationAsync(registration);
...........
}
// Send notification - target specific user
public async Task<OperationResult<bool>> Send(Platforms platform, string userName, INotificationMessage message)
{
...........
}
Just after submitting this question I tried updating tags from VS notification explorer. There I found that tags does not allowed blank spaces and my tags format in api call has spaces. These spaces are the main culprit. API call silently ignore these invalid tag formats
Here is complete working implementation. Modify according to your need
public class MyAzureNotificationHubManager
{
private Microsoft.ServiceBus.Notifications.NotificationHubClient _hub;
public MyAzureNotificationHubManager()
{
_hub = MyAzureNotificationClient.Instance.Hub;
}
private const string TAGFORMAT = "username:{0}";
public async Task<string> GetRegistrationIdAsync(string handle)
{
if (string.IsNullOrEmpty(handle))
throw new ArgumentNullException("handle could not be empty or null");
// This is requied - to make uniform handle format, otherwise could have some issue.
handle = handle.ToUpper();
string newRegistrationId = null;
// make sure there are no existing registrations for this push handle (used for iOS and Android)
var registrations = await _hub.GetRegistrationsByChannelAsync(handle, 100);
foreach (RegistrationDescription registration in registrations)
{
if (newRegistrationId == null)
{
newRegistrationId = registration.RegistrationId;
}
else
{
await _hub.DeleteRegistrationAsync(registration);
}
}
if (newRegistrationId == null)
newRegistrationId = await _hub.CreateRegistrationIdAsync();
return newRegistrationId;
}
public async Task UnRegisterDeviceAsync(string handle)
{
if (string.IsNullOrEmpty(handle))
throw new ArgumentNullException("handle could not be empty or null");
// This is requied - to make uniform handle format, otherwise could have some issue.
handle = handle.ToUpper();
// remove all registration by that handle
var registrations = await _hub.GetRegistrationsByChannelAsync(handle, 100);
foreach (RegistrationDescription registration in registrations)
{
await _hub.DeleteRegistrationAsync(registration);
}
}
public async Task RegisterDeviceAsync(string userName, string registrationId, Platforms platform, string handle)
{
if (string.IsNullOrEmpty(handle))
throw new ArgumentNullException("handle could not be empty or null");
// This is requied - to make uniform handle format, otherwise could have some issue.
handle = handle.ToUpper();
RegistrationDescription registration = null;
switch (platform)
{
case Platforms.MPNS:
registration = new MpnsRegistrationDescription(handle);
break;
case Platforms.WNS:
registration = new WindowsRegistrationDescription(handle);
break;
case Platforms.APNS:
registration = new AppleRegistrationDescription(handle);
break;
case Platforms.GCM:
registration = new GcmRegistrationDescription(handle);
break;
default:
throw new ArgumentException("Invalid device platform. It should be one of 'mpns', 'wns', 'apns' or 'gcm'");
}
registration.RegistrationId = registrationId;
// add check if user is allowed to add these tags
registration.Tags = new HashSet<string>();
registration.Tags.Add(string.Format(TAGFORMAT, userName));
// collect final registration
var result = await _hub.CreateOrUpdateRegistrationAsync(registration);
var output = new RegistrationOutput()
{
Platform = platform,
Handle = handle,
ExpirationTime = result.ExpirationTime,
RegistrationId = result.RegistrationId
};
if (result.Tags != null)
{
output.Tags = result.Tags.ToList();
}
}
public async Task<bool> Send(Platforms platform, string receiverUserName, INotificationMessage message)
{
string[] tags = new[] { string.Format(TAGFORMAT, receiverUserName) };
NotificationOutcome outcome = null;
switch (platform)
{
// Windows 8.1 / Windows Phone 8.1
case Platforms.WNS:
outcome = await _hub.SendWindowsNativeNotificationAsync(message.GetWindowsMessage(), tags);
break;
case Platforms.APNS:
outcome = await _hub.SendAppleNativeNotificationAsync(message.GetAppleMessage(), tags);
break;
case Platforms.GCM:
outcome = await _hub.SendGcmNativeNotificationAsync(message.GetAndroidMessage(), tags);
break;
}
if (outcome != null)
{
if (!((outcome.State == NotificationOutcomeState.Abandoned) || (outcome.State == NotificationOutcomeState.Unknown)))
{
return true;
}
}
return false;
}
}
public class MyAzureNotificationClient
{
// Lock synchronization object
private static object syncLock = new object();
private static MyAzureNotificationClient _instance { get; set; }
// Singleton inistance
public static MyAzureNotificationClient Instance
{
get
{
if (_instance == null)
{
lock (syncLock)
{
if (_instance == null)
{
_instance = new MyAzureNotificationClient();
}
}
}
return _instance;
}
}
public NotificationHubClient Hub { get; private set; }
private MyAzureNotificationClient()
{
Hub = Microsoft.ServiceBus.Notifications.NotificationHubClient.CreateClientFromConnectionString("<full access notification connection string>", "<notification hub name>");
}
}
public interface INotificationMessage
{
string GetAppleMessage();
string GetAndroidMessage();
string GetWindowsMessage();
}
public class SampleMessage : INotificationMessage
{
public string Message { get; set; }
public string GetAndroidMessage()
{
var notif = JsonObject.Create()
.AddProperty("data", data =>
{
data.AddProperty("message", this.Message);
});
return notif.ToJson();
}
public string GetAppleMessage()
{
// Refer - https://github.com/paultyng/FluentJson.NET
var alert = JsonObject.Create()
.AddProperty("aps", aps =>
{
aps.AddProperty("alert", this.Message ?? "Your information");
});
return alert.ToJson();
}
public string GetWindowsMessage()
{
// Refer - http://improve.dk/xmldocument-fluent-interface/
var msg = new XmlObject()
.XmlDeclaration()
.Node("toast").Within()
.Node("visual").Within()
.Node("binding").Attribute("template", "ToastText01").Within()
.Node("text").InnerText("Message here");
return msg.GetOuterXml();
}
}

Orchard Custom Settings Not Persisting

I'm pulling my hair out on this one; it should be so simple yet I can't figure out the issue.
I'm trying to simply save some custom settings in my module. I used the Orchard.Email module as an example on how to plug into the 'Settings' menu; my code is as follows:
Migrations.cs
public class CustomSettingsMigrations : DataMigrationImpl {
public int Create() {
SchemaBuilder.CreateTable("CustomSettingsPartRecord", table => table
.ContentPartRecord()
.Column<string>("GatewayUrl")
.Column<string>("MerchantId")
.Column<string>("MerchantPassword")
.Column<bool>("SandboxMode")
.Column<string>("SandboxGatewayUrl")
.Column<string>("SandboxMerchantId")
.Column<string>("SandboxMerchantPassword")
);
return 1;
}
}
Models/CustomSettingsPartRecord.cs
public class CustomSettingsPartRecord : ContentPartRecord {
public virtual string GatewayUrl { get; set; }
public virtual string MerchantId { get; set; }
public virtual string MerchantPassword { get; set; }
public virtual bool SandboxMode { get; set; }
public virtual string SandboxGatewayUrl { get; set; }
public virtual string SandboxMerchantId { get; set; }
public virtual string SandboxMerchantPassword { get; set; }
public CustomSettingsPartRecord() {
SandboxMode = true;
}
}
Models/CustomSettingsPart.cs
public class CustomSettingsPart : ContentPart<CustomSettingsPartRecord> {
private readonly ComputedField<string> _password = new ComputedField<string>();
public ComputedField<string> PasswordField {
get { return _password; }
}
public string GatewayUrl {
get { return Record.GatewayUrl; }
set { Record.GatewayUrl = value; }
}
public string MerchantId {
get { return Record.MerchantId; }
set { Record.MerchantId = value; }
}
public string MerchantPassword {
get { return Record.MerchantPassword; }
set { Record.MerchantPassword = value; }
}
public bool SandboxMode {
get { return Record.SandboxMode; }
set { Record.SandboxMode = value; }
}
public string SandboxGatewayUrl {
get { return Record.SandboxGatewayUrl; }
set { Record.SandboxGatewayUrl = value; }
}
public string SandboxMerchantId {
get { return Record.SandboxMerchantId; }
set { Record.SandboxMerchantId = value; }
}
public string SandboxMerchantPassword {
get { return Record.SandboxMerchantPassword; }
set { Record.SandboxMerchantPassword = value; }
}
public bool IsValid() {
return ((!String.IsNullOrWhiteSpace(Record.GatewayUrl)
&& !String.IsNullOrWhiteSpace(Record.MerchantId)) ||
(Record.SandboxMode && !String.IsNullOrWhiteSpace(Record.SandboxGatewayUrl) &&
!String.IsNullOrWhiteSpace(Record.SandboxMerchantId)));
}
}
Handlers/CustomSettingsPartHandler.cs
[UsedImplicitly]
public class CustomSettingsPartHandler : ContentHandler {
private readonly IEncryptionService _encryptionService;
public CustomSettingsPartHandler(IRepository<CustomSettingsPartRecord> repository, IEncryptionService encryptionService) {
T = NullLocalizer.Instance;
Logger = NullLogger.Instance;
_encryptionService = encryptionService;
Filters.Add(new ActivatingFilter<CustomSettingsPart>("Site"));
Filters.Add(StorageFilter.For(repository));
OnLoaded<CustomSettingsPart>(LazyLoadHandlers);
}
public Localizer T { get; set; }
public new ILogger Logger { get; set; }
void LazyLoadHandlers(LoadContentContext context, CustomSettingsPart part) {
part.PasswordField.Getter(() => {
try {
return String.IsNullOrWhiteSpace(part.Record.MerchantPassword) ? String.Empty : Encoding.UTF8.GetString(_encryptionService.Decode(Convert.FromBase64String(part.Record.MerchantPassword)));
}
catch (Exception) {
Logger.Error("The merchant password could not be decrypted. It might be corrupt, try to reset it.");
return null;
}
});
part.PasswordField.Setter(value => part.Record.MerchantPassword = String.IsNullOrWhiteSpace(value) ? String.Empty : Convert.ToBase64String(_encryptionService.Encode(Encoding.UTF8.GetBytes(value))));
}
protected override void GetItemMetadata(GetContentItemMetadataContext context) {
if (context.ContentItem.ContentType != "Site")
return;
base.GetItemMetadata(context);
context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("Custom")));
}
}
Drivers/CustomSettingsPartDriver.cs
public class CustomSettingsPartDriver : ContentPartDriver<CustomSettingsPart> {
private const string TemplateName = "Parts/CustomSettings";
public CustomSettingsPartDriver() {
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
protected override string Prefix { get { return "CustomSettings"; } }
protected override DriverResult Editor(CustomSettingsPart part, dynamic shapeHelper) {
return ContentShape("Parts_CustomSettings_Edit",
() => shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: part, Prefix: Prefix))
.OnGroup("custom");
}
protected override DriverResult Editor(CustomSettingsPart part, IUpdateModel updater, dynamic shapeHelper) {
return ContentShape("Parts_CustomSettings_Edit", () => {
var previousPassword = part.MerchantPassword;
updater.TryUpdateModel(part, Prefix, null, null);
// restore password if the input is empty, meaning it has not been changed
if (String.IsNullOrEmpty(part.MerchantPassword)) {
part.MerchantPassword = previousPassword;
}
return shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: part, Prefix: Prefix)
.OnGroup("custom");
});
}
}
Views/EditorTemplates/Parts/CustomSettings.cshtml
#model CustomModule.Models.CustomSettingsPart
#{
Script.Require("jQuery");
}
<fieldset>
<legend>#T("Custom Settings")</legend>
<div>
<label for="#Html.FieldIdFor(m => m.GatewayUrl)">#T("Gateway Url")</label>
#Html.EditorFor(m => m.GatewayUrl)
#Html.ValidationMessage("GatewayUrl", "*")
</div>
<div>
<label for="#Html.FieldIdFor(m => m.MerchantId)">#T("Merchant ID")</label>
#Html.EditorFor(m => m.MerchantId)
#Html.ValidationMessage("MerchantId", "*")
</div>
<div>
<label for="#Html.FieldIdFor(m => m.MerchantPassword)">#T("Merchant Password")</label>
#Html.PasswordFor(m => m.MerchantPassword)
#Html.ValidationMessage("MerchantPassword", "*")
</div>
<div>
#Html.EditorFor(m => m.SandboxMode)
<label for="#Html.FieldIdFor(m => m.SandboxMode)" class="forcheckbox">#T("Enable Sandbox Mode (for testing)")</label>
#Html.ValidationMessage("SandboxMode", "*")
</div>
<div id="sandboxSettings">
<div>
<label for="#Html.FieldIdFor(m => m.SandboxGatewayUrl)">#T("Sandbox Gateway Url")</label>
#Html.EditorFor(m => m.SandboxGatewayUrl)
#Html.ValidationMessage("SandboxGatewayUrl", "*")
</div>
<div>
<label for="#Html.FieldIdFor(m => m.SandboxMerchantId)">#T("Sandbox Merchant ID")</label>
#Html.EditorFor(m => m.SandboxMerchantId)
#Html.ValidationMessage("SandboxMerchantId", "*")
</div>
<div>
<label for="#Html.FieldIdFor(m => m.SandboxMerchantPassword)">#T("Sandbox Merchant Password")</label>
#Html.EditorFor(m => m.SandboxMerchantPassword)
#Html.ValidationMessage("SandboxMerchantPassword", "*")
</div>
</div>
</fieldset>
#using (Script.Foot()) {
<script>
$('##Html.FieldIdFor(m => m.SandboxMode)').on('click', function() {
$('#sandboxSettings').toggle($(this).prop('checked'));
});
</script>
}
I have the Placement.info and I can access the View through the "Custom" menu item underneath "Settings" in the main menu. The View loads fine, and when I enter some details and click 'Save', the form is sent find and will hit the CustomSettingsPartDriver.cs DriverResult Editor(CustomSettingsPart part, IUpdateModel updater.. method.
I believe this is where the issue could be, as it doesn't hit any breakpoints inside the return ContentShape("Parts_CustomSettings_Edit, () => { lambda expression.
Could anyone shed any light on how I can resolve this? I'm sure it's a simple issue, but I've been trying to figure this one out for a while unsuccessfully. Thanks!
Okay, I managed to figure this one out. I feel silly asking questions then answering them myself, but if it saves one person time in the future, then it's worth it.
The issue was, as I expected, in the Driver. I appended the .OnGroup() extension to the wrong Shape.
Below is the fixed Driver code:
protected override DriverResult Editor(CustomSettingsPart part, IUpdateModel updater, dynamic shapeHelper) {
return ContentShape("Parts_CustomSettings_Edit", () => {
var previousPassword = part.MerchantPassword;
updater.TryUpdateModel(part, Prefix, null, null);
// restore password if the input is empty, meaning it has not been changed
if (String.IsNullOrEmpty(part.MerchantPassword)) {
part.MerchantPassword = previousPassword;
}
return shapeHelper.EditorTemplate(TemplateName: TemplateName, Model: part, Prefix: Prefix);
// Offending extension -> .OnGroup("custom");
}).OnGroup("custom"); // In the correct location
}
*facepalm*

Resources