ASP.NET Core 2.2 - SignalR How to join a group and send message from the server? - asp.net-core

I have a set up a basic solution for SignalR where I can send and receive messages to all clients through the browser.
Required solution:
I need to implement the use of groups, BUT with the ability to join the group and send a message from a background service class that is running on the server.
Currently I have only been successful in sending a message to ALL clients from the server.
My background service class (basically a comms server) will be running multiple instances, so each instance will have a unique name such as CommsServer 1, CommsServer 2 etc. Each instance of the comms server will need to output messages to a specific SignalR group of recipients.
In the browser, the user will select which SignalR group they wish to join from a dropdown list that is pulled from the server. The server also has knowledge of this same list and therefore each comms server instance will represent an item from the list.
My code so far:
MessageHub Class:
public class MessageHub : Hub
{
public Task SendMessageToAll(string message)
{
return Clients.All.SendAsync("ReceiveMessage", message);
}
}
Message.js File:
"use strict";
var connection = new signalR.HubConnectionBuilder()
.withUrl("/messages")
.build();
connection.on("ReceiveMessage", function (message) {
var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
var div = document.createElement("div");
div.innerHTML = msg + "<hr/>";
document.getElementById("messages").appendChild(div);
});
connection.start().catch(function (err) {
return console.error(err.toString());
});
document.getElementById("sendButton").addEventListener("click", function (event) {
var message = document.getElementById("message").value;
connection.invoke("SendMessageToAll", message).catch(function (err) {
return console.error(err.toString());
});
e.preventDefault();
});
Razor Page:
<textarea name="message" id="message"></textarea>
<input type="button" id="sendButton" value="Send Message" />
<div id="messages"></div>
<script src="~/lib/signalr/dist/browser/signalr.js"></script>
<script src="~/js/message.js"></script>
Comms server class:
public class TcpServer
{
private readonly IHubContext<MessageHub> _hubContext;
public TcpServerTcpServer(IHubContext<MessageHub> hubContext)
{
_hubContext = hubContext;
}
public string inboundIpAddress = "127.0.0.1";
public int inboundLocalPortNumber = 10001;
public void TcpServerIN(string serverName)
{
Task.Run(() =>
{
IPAddress localAddrIN = IPAddress.Parse(inboundIpAddress);
TcpListener listener = new TcpListener(localAddrIN, inboundLocalPortNumber);
listener.Start();
while (true)
{
TcpClient client = listener.AcceptTcpClient();
// Get a stream object for reading and writing
NetworkStream stream = client.GetStream(); // Networkstream is used to send/receive messages
//Buffer for reading data
Byte[] bytes = new Byte[4096];
String data = null;
int i;
// Loop to receive all the data sent by the client.
while ((i = stream.Read(bytes, 0, bytes.Length)) != 0)
{
// Translate data bytes to a ASCII string.
data = System.Text.Encoding.ASCII.GetString(bytes, 0, i);
_hubContext.Clients.All.SendAsync(data);
byte[] msg = System.Text.Encoding.ASCII.GetBytes(data);
stream.Write(msg, 0, data.Length);
}
// Shutdown and end connection
client.Close();
}
});
}
}
I need to select a specific SignalR group name e.g. "CommsServer 1" this will form the name of the SignalR group that the user can select from a dropdown list in the browser so they can monitor events from just this one particular server instance. Other user may wish to monitor events from a different server instance.
One option I was considering is simply send events from all servers instances to ALL browser connected clients and then somehow filter the events on the client side, but this would seem an inefficient way of handling things and not good for network traffic.

If you want your client to decide which group to join (by selecting from a list of dropdowns), then your client should probably notify the server of its subscription to messages aimed at the group via the hub:
Hub:
public class MessageHub : Hub
{
public async Task SendMessageToAll(string message)
{
await Clients.All.SendAsync("ReceiveMessage", message);
}
public async Task Subscribe(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
}
Background service:
public class TcpServer
{
private readonly IHubContext<MessageHub> _hubContext;
private readonly string _serviceInstanceName;
public TcpServerTcpServer(IHubContext<MessageHub> hubContext, string serviceInstanceName)
{
_hubContext = hubContext;
_serviceInstanceName = serviceInstanceName;
}
public async Task SendMessageToGroup(string message)
{
await _hubContext.Clients.Group(_serviceInstanceName).SendAsync("ReceiveMessage", message);
}
}
And somewhere in your client, perhaps after user chooses an option from your dropdown:
connection.invoke("subscribe", "CommsServer 1, or whatever").catch(function (err) {
return console.error(err.toString());
});

Related

Is there is a way to connect devices from Azure IoT Hub to Azure IoT Central?

i see so many devices that can easily connect to Azure IoT hub via MQTT. But it is NOT as easy to connect those same devices to Azure IoT Central. Is there a way to send those data from Azure IoT Hub to Azure IoT Central?
In the case of sending only a telemetry data to the Azure IoT Central App, you can use the Azure Event Grid integrator, where the device telemetry message is published via the Azure IoT Hub routing feature:
The following code snippet is an example of the webhook subscriber implementation (HttpTrigger Function) for handling all needs such as DPS, etc.
function.json file:
{
"bindings": [
{
"name": "eventGridEvent",
"authLevel": "function",
"methods": [
"post",
"options"
],
"direction": "in",
"type": "httpTrigger"
},
{
"name": "$return",
"type": "http",
"direction": "out"
}
]
}
run.csx file:
#r "Newtonsoft.Json"
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Net;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
public static async Task<ActionResult> Run(JObject eventGridEvent, HttpRequest req, ILogger log)
{
if (req.Method == HttpMethod.Options.ToString())
{
log.LogInformation("CloudEventSchema validation");
req.HttpContext.Response.Headers.Add("Webhook-Allowed-Origin", req.Headers["WebHook-Request-Origin"].FirstOrDefault()?.Trim());
return (ActionResult)new OkResult();
}
// consumer of telemetry (iot central)
uint sasTokenTTLInHrs = 1;
string iotcScopeId = req.Headers["iotc-scopeId"].FirstOrDefault() ?? Environment.GetEnvironmentVariable("AzureIoTC_scopeId");
string iotcSasToken = req.Headers["iotc-sasToken"].FirstOrDefault() ?? Environment.GetEnvironmentVariable("AzureIoTC_sasToken");
log.LogInformation($"CloudEvent_Id = {eventGridEvent["id"]}");
log.LogInformation($"AzureIoT_scopeId = {iotcScopeId}");
// mandatory properties
string source = eventGridEvent["data"]?["systemProperties"]?["iothub-message-source"]?.Value<string>();
string deviceId = eventGridEvent["data"]?["systemProperties"]?["iothub-connection-device-id"]?.Value<string>();
if (source == "Telemetry" && !string.IsNullOrEmpty(deviceId) && Regex.IsMatch(deviceId, #"^[a-z0-9\-]+$"))
{
var sysProp = eventGridEvent["data"]["systemProperties"];
var appProp = eventGridEvent["data"]["properties"];
// device model
var component = appProp?["iothub-app-component-name"]?.Value<string>() ?? sysProp["dt-subject"]?.Value<string>() ?? "";
var modelId = appProp?["iothub-app-model-id"]?.Value<string>() ?? sysProp["dt-dataschema"]?.Value<string>();
// creation time
var enqueuedtime = sysProp["iothub-enqueuedtime"]?.Value<DateTime>().ToString("o");
var ctime = appProp?["iothub-creation-time-utc"]?.Value<DateTime>().ToString("o");
// device group (device prefix)
var deviceGroup = appProp?["iothub-app-device-group"]?.Value<string>();
deviceId = $"{(deviceGroup == null ? "" : deviceGroup + "-")}{deviceId}";
// remove sysprop
((JObject)eventGridEvent["data"]).Remove("systemProperties");
try
{
var info = await Connectivity.GetConnectionInfo(deviceId, modelId, iotcScopeId, iotcSasToken, log, sasTokenTTLInHrs);
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", info.SasToken);
client.DefaultRequestHeaders.Add("dt-subject", component);
client.DefaultRequestHeaders.Add("iothub-app-iothub-creation-time-utc", ctime ?? enqueuedtime);
var response = await client.PostAsJsonAsync(info.RequestUri, eventGridEvent["data"]);
response.EnsureSuccessStatusCode();
}
log.LogInformation($"POST: {info.RequestUri}\r\n{eventGridEvent["data"]}");
}
catch(Exception ex)
{
log.LogError(ex.InnerException == null ? ex.Message : ex.InnerException.Message);
Connectivity.RemoveDevice(deviceId);
throw ex; // for retrying and deadlettering undeliverable message
}
}
else
{
log.LogWarning($"Wrong event message:\r\n{eventGridEvent}");
}
return (ActionResult)new OkResult();
}
class ConnectivityInfo
{
public string IoTHubName { get; set; }
public string RequestUri { get; set; }
public string SasToken { get; set; }
public ulong SaSExpiry { get; set; }
public string ModelId { get; set; }
public string DeviceConnectionString { get; set; }
}
static class Connectivity
{
static Dictionary<string, ConnectivityInfo> devices = new Dictionary<string, ConnectivityInfo>();
public static async Task<ConnectivityInfo> GetConnectionInfo(string deviceId, string modelId, string iotcScopeId, string iotcSasToken, ILogger log, uint sasTokenTTLInHrs = 24, int retryCounter = 10, int pollingTimeInSeconds = 3)
{
if (devices.ContainsKey(deviceId))
{
if (!string.IsNullOrEmpty(modelId) && devices[deviceId].ModelId != modelId)
{
log.LogWarning($"Reprovissiong device with new model");
devices.Remove(deviceId);
}
else
{
if (!SharedAccessSignatureBuilder.IsValidExpiry(devices[deviceId].SaSExpiry, 100))
{
log.LogWarning($"Refreshing sasToken");
devices[deviceId].SasToken = SharedAccessSignatureBuilder.GetSASTokenFromConnectionString(devices[deviceId].DeviceConnectionString, sasTokenTTLInHrs);
devices[deviceId].SaSExpiry = ulong.Parse(SharedAccessSignatureBuilder.GetExpiry(sasTokenTTLInHrs));
}
return devices[deviceId];
}
}
string deviceKey = SharedAccessSignatureBuilder.ComputeSignature(iotcSasToken, deviceId);
string address = $"https://global.azure-devices-provisioning.net/{iotcScopeId}/registrations/{deviceId}/register?api-version=2021-06-01";
string sas = SharedAccessSignatureBuilder.GetSASToken($"{iotcScopeId}/registrations/{deviceId}", deviceKey, "registration", 1);
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Add("Authorization", sas);
client.DefaultRequestHeaders.Add("accept", "application/json");
string jsontext = string.IsNullOrEmpty(modelId) ? null : $"{{ \"modelId\":\"{modelId}\" }}";
var response = await client.PutAsync(address, new StringContent(JsonConvert.SerializeObject(new { registrationId = deviceId, payload = jsontext }), Encoding.UTF8, "application/json"));
var atype = new { errorCode = "", message = "", operationId = "", status = "", registrationState = new JObject() };
do
{
dynamic operationStatus = JsonConvert.DeserializeAnonymousType(await response.Content.ReadAsStringAsync(), atype);
if (!string.IsNullOrEmpty(operationStatus.errorCode))
{
throw new Exception($"{operationStatus.errorCode} - {operationStatus.message}");
}
response.EnsureSuccessStatusCode();
if (operationStatus.status == "assigning")
{
Task.Delay(TimeSpan.FromSeconds(pollingTimeInSeconds)).Wait();
address = $"https://global.azure-devices-provisioning.net/{iotcScopeId}/registrations/{deviceId}/operations/{operationStatus.operationId}?api-version=2021-06-01";
response = await client.GetAsync(address);
}
else if (operationStatus.status == "assigned")
{
var cinfo = new ConnectivityInfo();
cinfo.ModelId = modelId;
cinfo.IoTHubName = operationStatus.registrationState.assignedHub;
cinfo.DeviceConnectionString = $"HostName={cinfo.IoTHubName};DeviceId={deviceId};SharedAccessKey={deviceKey}";
cinfo.RequestUri = $"https://{cinfo.IoTHubName}/devices/{deviceId}/messages/events?api-version=2021-04-12";
cinfo.SasToken = SharedAccessSignatureBuilder.GetSASToken($"{cinfo.IoTHubName}/{deviceId}", deviceKey, null, sasTokenTTLInHrs);
cinfo.SaSExpiry = ulong.Parse(SharedAccessSignatureBuilder.GetExpiry(sasTokenTTLInHrs));
devices.Add(deviceId, cinfo);
log.LogInformation($"DeviceConnectionString: {cinfo.DeviceConnectionString}");
return cinfo;
}
else
{
throw new Exception($"{operationStatus.registrationState.status}: {operationStatus.registrationState.errorCode} - {operationStatus.registrationState.errorMessage}");
}
} while (--retryCounter > 0);
throw new Exception("Registration device status retry timeout exprired, try again.");
}
}
public static void RemoveDevice(string deviceId)
{
if (devices.ContainsKey(deviceId))
devices.Remove(deviceId);
}
}
public sealed class SharedAccessSignatureBuilder
{
public static string GetHostNameNamespaceFromConnectionString(string connectionString)
{
return GetPartsFromConnectionString(connectionString)["HostName"].Split('.').FirstOrDefault();
}
public static string GetSASTokenFromConnectionString(string connectionString, uint hours = 24)
{
var parts = GetPartsFromConnectionString(connectionString);
if (parts.ContainsKey("HostName") && parts.ContainsKey("SharedAccessKey"))
return GetSASToken(parts["HostName"], parts["SharedAccessKey"], parts.Keys.Contains("SharedAccessKeyName") ? parts["SharedAccessKeyName"] : null, hours);
else
return string.Empty;
}
public static string GetSASToken(string resourceUri, string key, string keyName = null, uint hours = 24)
{
try
{
var expiry = GetExpiry(hours);
string stringToSign = System.Web.HttpUtility.UrlEncode(resourceUri) + "\n" + expiry;
var signature = SharedAccessSignatureBuilder.ComputeSignature(key, stringToSign);
var sasToken = keyName == null ?
String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}", System.Web.HttpUtility.UrlEncode(resourceUri), System.Web.HttpUtility.UrlEncode(signature), expiry) :
String.Format(CultureInfo.InvariantCulture, "SharedAccessSignature sr={0}&sig={1}&se={2}&skn={3}", System.Web.HttpUtility.UrlEncode(resourceUri), System.Web.HttpUtility.UrlEncode(signature), expiry, keyName);
return sasToken;
}
catch
{
return string.Empty;
}
}
#region Helpers
public static string ComputeSignature(string key, string stringToSign)
{
using (HMACSHA256 hmac = new HMACSHA256(Convert.FromBase64String(key)))
{
return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(stringToSign)));
}
}
public static Dictionary<string, string> GetPartsFromConnectionString(string connectionString)
{
return connectionString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Split(new[] { '=' }, 2)).ToDictionary(x => x[0].Trim(), x => x[1].Trim(), StringComparer.OrdinalIgnoreCase);
}
// default expiring = 24 hours
public static string GetExpiry(uint hours = 24)
{
TimeSpan sinceEpoch = DateTime.UtcNow - new DateTime(1970, 1, 1);
return Convert.ToString((ulong)sinceEpoch.TotalSeconds + 3600 * hours);
}
public static DateTime GetDateTimeUtcFromExpiry(ulong expiry)
{
return (new DateTime(1970, 1, 1)).AddSeconds(expiry);
}
public static bool IsValidExpiry(ulong expiry, ulong toleranceInSeconds = 0)
{
return GetDateTimeUtcFromExpiry(expiry) - TimeSpan.FromSeconds(toleranceInSeconds) > DateTime.UtcNow;
}
#endregion
}
The following screen snippet shows part of the subscription for passing requested headers for webhook subscriber:
Note, that the mapping feature can be used at the Azure IoT Central App on the input side, based on the device model.
As the above first picture shows, this solution is based on using the Azure Event Grid feature, where the Azure IoT Hub represents a publisher of the device telemetry data and the Azure IoT Central app is its consumer.
The logical connectivity between the Azure IoT Hub and Azure IoT Central is done via the AEG Subscription with a webhook destination handler such as the HttpTrigger Function (see the above implementation).
Note, that this subscription is configured for delivering an event message (device telemetry data) in the CloudEventSchema.
Device that provisions itself in IoTHub via DPS will work with IoT Central with no change other than ID Scope sent by device during provisioning which identifies DPS service instance. One ID Scope will point it specific IoT Hub configured in DPS enrollment group, while other will point it to an internal IoT Hub in IoT Central application (IoT Central spins additional internal IoT Hubs as needed for auto scaling, which is why it has its own internal DPS).
Use of DPS allows provisioning of the device to specific IoTHub at the first call and subsequently the change can be triggered explicitly for reprovisioning to different IoTHub or IoT Central, which can be used to move device if needed. This functionality allows scenarios where you can force a device to connect to an IoT Hub or IoT Central by implementing ID Scope change direct method and triggering reprovisioning. Use of DPS is highly recommended as it simplifies provisioning and provides this flexibility.
Reprovisioning should be part of retry logic on the device in case if it fails to connect to IoTHub for certain amount of time in addition to on-demand change described above.
What makes you think "But it is NOT as easy to connect those same devices to Azure IoT Central"?
Any device connecting to IoTHub can also connect to IoTCentral, you just need to provision the device using DPS, it will get the IoTHub hostname and everything else will work the same way.

Cannot Interrupt HttpClient.GetStreamAsync by means of CancellationToken in ASP.NET Core

I'm trying to send an http request to an AXIS Camera in order to receive a stream.
Everything works fine except that I can't get to use CancellationToken to cancel the request when it is no more needed. I've the following architecture:
Blazor client:
// LiveCamera.razor
<img src="CameraSystem/getStream" onerror="[...]" alt="">
ASP.NET Core Server:
// CameraSystemController.cs
[ApiController]
[Route("[controller]")]
public class CameraSystemController : Controller
{
[HttpGet("getStream")]
public async Task<IActionResult> GetStream()
{
Stream stream = await Device_CameraStandard.GetStream();
if (stream != null) {
Response.Headers.Add("Cache-Control", "no-cache");
FileStreamResult result = new FileStreamResult(stream, _contentTypeStreaming) {
EnableRangeProcessing = true
};
return result;
} else {
return new StatusCodeResult((int)HttpStatusCode.ServiceUnavailable);
}
}
}
Class accessing the camera:
// Device_CameraStandard.cs
internal class Device_CameraStandard
{
private HttpClient _httpClient;
private static CancellationTokenSource _tokenSource;
private System.Timers.Timer _keepAliveTimer;
internal Device_CameraStandard() {
_keepAliveTimer = new System.Timers.Timer();
_keepAliveTimer.Interval = 3000;
_keepAliveTimer.Elapsed += KeepAliveTimeout;
_tokenSource = new CancellationTokenSource();
[...]
}
internal async Task<Stream> GetStream()
{
return await _httpClient.GetStreamAsync("http://[...]/axis-cgi/mjpg/video.cgi?&camera=1", _tokenSource.Token);
}
// Invoked periodically by client from LiveCamera.razor.cs, not included here
internal void KeepAlive()
{
LLogger.Debug("KeepAlive!");
_keepAliveTimer.Stop();
_keepAliveTimer.Start();
}
private void KeepAliveTimeout(object sender, ElapsedEventArgs e)
{
LLogger.Debug("Timeout!");
_keepAliveTimer.Stop();
_tokenSource.Cancel();
_tokenSource.Dispose();
_tokenSource = new CancellationTokenSource();
}
}
However, even if all clients leave LiveCamera.razor page and the _keepAliveTimer elapses and the CancellationTokenSource is canceled, the request is not canceled. I can see it from the fact that bandwidth usage does not decreases (the "receiving" bandwitdh, indicating that Server is still receiving data from camera), it only decreases if I close the browser tab.
Could you please help me to understand what am I doing wrong? Thanks
EDIT: In the end, even after following the suggestion of observing the token in all code parts where the returned stream was used, included the controller, I ended up discovering that the tag
// LiveCamera.razor
<img src="CameraSystem/getStream" onerror="[...]" alt="">
was causing the client to never stop sending requests. Thus I had to use a workaround to force client to stop sending requests before leaving LiveCamera.razor page.

Cannot send data if the connection is not in the 'Connected' State. - Multiple Hubs in SignalR

I'm attempting to get to grips with SignalR, to do so I'm trying to extend the functionality of the simple chat room tutorial that Microsoft provide in their documentation.
I'm now trying to add a second hub, which will allow the user to do send in integers and receive the value multiplied by 10. The hub itself is almost identical to the normal ChatHub, except with an extra step that checks the input is a number and does the multiplication.
ChatHub
public class ChatHub : Hub
{
public async Task SendMessage(string group,string user, string message)
{
await Clients.Group(group).SendAsync("ReceiveMessage", user, message);
}
public async Task AddToGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has joined the group {groupName}.");
}
}
CalcHub
public class CalcHub : Hub
{
public async Task SendMessage(string group, string user, string message)
{
var value = MultiplyByTen(message);
await Clients.Group(group).SendAsync("ReceiveMessage", user, value);
}
public async Task AddToGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync("Send", $"{Context.ConnectionId} has joined the group {groupName}.");
}
public string MultiplyByTen(string input)
{
bool isANumber = Int32.TryParse(input, out int value);
if (isANumber)
{
return (value * 10).ToString();
}
return "Not a number";
}
}
I have Javascript set up for my front-end, which works perfectly fine when I try to connect to the ChatHub and send messages, however when I attempt to use the connection to CalcHub, I get the Cannot send data if the connection is not in the 'Connected' State error message.
Here is how the two connections are established.
var calcConnection = new signalR.HubConnectionBuilder().withUrl("https://localhost:44309/calcHub").build();
var chatConnection = new signalR.HubConnectionBuilder().withUrl("https://localhost:44308/chatHub").build();
var activeConnection;
setConnection();
$("#hubSelector").on("change",
function(data) {
setConnection();
});
I have a simple select element that will swap the connection based on its value. SetConnection is the method that controls this, which is used at DOM ready to set the initial connection.
Both of the hubs are registered in my startup class too.
app.UseSignalR(routes =>
{
routes.MapHub<ChatHub>("/chatHub");
routes.MapHub<CalcHub>("/calcHub");
});
If I navigate to the two addresses of the hubs https://localhost:44309/calcHub and https://localhost:44309/chatHub, I can also see that they are valid addresses as I get the Connection ID required message.
Why is my calcHub not working?
Site.js
// Please see documentation at https://learn.microsoft.com/aspnet/core/client-side/bundling-and-minification
// for details on configuring this project to bundle and minify static web assets.
// Write your JavaScript code.
$(function() {
var calcConnection = new signalR.HubConnectionBuilder().withUrl("https://localhost:44309/calcHub").build();
var chatConnection = new signalR.HubConnectionBuilder().withUrl("https://localhost:44308/chatHub").build();
var activeConnection;
//setConnection();
//$("#hubSelector").on("change",
// function(data) {
// setConnection();
// });
activeConnection.on("ReceiveMessage", function (user, message) {
var msg = message.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
var encodedMsg = user + " says " + msg;
var li = document.createElement("li");
li.textContent = encodedMsg;
document.getElementById("messagesList").appendChild(li);
});
activeConnection.start().catch(function (err) {
return console.error(err.toString());
});
$("#addgroup").on("click", function () {
var group = document.getElementById("group").value;
activeConnection.invoke("AddToGroup", group).catch(function (err) {
return console.error(err.toString());
});
$("#group-list").append("<p>" + group + "</p>");
event.preventDefault();
});
$("#sendButton").on("click", function () {
var user = document.getElementById("userInput").value;
var message = document.getElementById("messageInput").value;
var group = document.getElementById("group").value;
activeConnection.invoke("SendMessage", group, user, message).catch(function (err) {
return console.error(err.toString());
});
event.preventDefault();
});
function setConnection() {
var selectValue = $("#hubSelector").val();
if (selectValue === "chat") {
$("#activeHub").html("<span>Active Hub: Chat</span>");
activeConnection = chatConnection;
}
if (selectValue === "calc") {
$("#activeHub").html("<span>Active Hub: Calc</span>");
activeConnection = calcConnection;
}
console.log(activeConnection);
}
});

Sharepoint 2010 Web Part Communication - How to make consumer wait for the provider

I have a series of web parts I need to implement in SharePoint 2010. The data provider web part uses an UpdatePanel and asynchronously makes a web service call which can potentially be slow. To keep it simple, I've put a single consumer web part on the page (Chart) which will use the consumer as its data provider.
My problem is that I can't get the consumer to wait for the provider - I get a variety of errors but all basically come back to "There is no data available". This may be because it is a Chart web part but the question also applies to the other custom parts I will be developing as they will pull the same data.
The question is: how do I either push data to my consumers when my provider is ready or somehow let them wait for my provider to have data (via polling or whatever).
Note: this is just a prototype, I haven't added error handling, etc yet.
Code is below:
[ToolboxItem(true)]
public partial class ClarityProjectGeneral : System.Web.UI.WebControls.WebParts.WebPart , IWebPartTable
{
public DataTable ProjectVitals = new DataTable(); For web part communication
// bunch of properties
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
InitializeControl();
// For web part communication
// Initialize our datatable so the chart doesn't barf
DataColumn col = new DataColumn();
col.DataType = typeof(string);
col.ColumnName = "Name";
this.ProjectVitals.Columns.Add(col);
col = new DataColumn();
col.DataType = typeof(DateTime);
col.ColumnName = "Start";
this.ProjectVitals.Columns.Add(col);
col = new DataColumn();
col.DataType = typeof(DateTime);
col.ColumnName = "End";
this.ProjectVitals.Columns.Add(col);
}
protected void Page_Load(object sender, EventArgs e)
{
loading.Visible = true;
content.Visible = false;
}
public ClarityObjectClasses.Projects GetProject(string projectID)
{
Clarity.ClarityAbstractorProject ca = new Clarity.ClarityAbstractorProject(this.Username, this.Password);
Dictionary<string, string> queryParams = new Dictionary<string, string>();
queryParams.Add("projectID", projectID);
// Class for making web service call
ClarityObjectClasses.Projects response = new ClarityObjectClasses.Projects();
response = ca.GetProject(queryParams);
return response;
}
protected void Timer1_Tick(object sender, EventArgs e)
{
if (this.ProjectID == null || this.Username == null || this.Password == null)
{
lblConfigError.Visible = true;
lblConfigError.Text = "One or more required configuration values are not set. Please check the web part configuration.";
panelProjectDetails.Visible = false;
}
else
{
loading.Visible = true;
content.Visible = false;
panelProjectDetails.Visible = true;
ClarityObjectClasses.Projects projects = GetProject(this.ProjectID);
//Assign a bunch of values
// For web part communication
LoadTable(projects.Project[0]);
Timer1.Enabled = false;
loading.Visible = false;
content.Visible = true;
}
}
/* Interface functions for Graph Chart communication */
For web part communication
protected void LoadTable(ClarityObjectClasses.Project project)
{
DataRow row = ProjectVitals.NewRow();
row["Name"] = project.name;
row["Start"] = project.start;
row["End"] = project.finish;
this.ProjectVitals.Rows.Add(row);
}
public PropertyDescriptorCollection Schema
{
get
{
return TypeDescriptor.GetProperties(ProjectVitals.DefaultView[0]);
}
}
public void GetTableData(TableCallback callback)
{
callback(ProjectVitals.Rows);
}
public bool ConnectionPointEnabled
{
get
{
object o = ViewState["ConnectionPointEnabled"];
return (o != null) ? (bool)o : true;
}
set
{
ViewState["ConnectionPointEnabled"] = value;
}
}
[ConnectionProvider("Table", typeof(TableProviderConnectionPoint), AllowsMultipleConnections = true)]
public IWebPartTable GetConnectionInterface()
{
return this;
}
public class TableProviderConnectionPoint : ProviderConnectionPoint
{
public TableProviderConnectionPoint(MethodInfo callbackMethod, Type interfaceType, Type controlType, string name, string id, bool allowsMultipleConnections)
: base(callbackMethod, interfaceType, controlType, name, id, allowsMultipleConnections)
{
}
public override bool GetEnabled(Control control)
{
return ((ClarityProjectGeneral)control).ConnectionPointEnabled;
}
}
}
Do not quite understand, but if it helps
You may not use "connectable" web-parts inside UpdatePanel,
because of lack of corresponding events to bind data on asynchronous callback.
I just stumbled across this. I had exactly the same problem trying to implement a custom webpart just as a proof to myself. I applied filters to both my webpart and a list, and then let a chart consume them. What I found was that my webpart sent the wrong data, but the list webpart worked as expected.
So I reflected the XsltListViewWebPart (or whatever it's exact name is) and I discovered that there is an IConnectionData interface. This allows you to specify the dependencies and get the correct delay binding you need. GetRequiresData indicates that there are still more connections to be consumed before the data can be requested.

Duplex WCF + Static Collection of COM objects

I am trying to build a WCF service that exposes the functionality of a particular COM object that I do not have the original source for. I am using duplex binding so that each client has their own instance as there are events tied to each particular instance which are delivered through a callback (IAgent). It appears there is a deadlock or something because after the first action, my service blocks at my second action's lock. I have tried implementing these custom STA attribute and operation behaviors (http://devlicio.us/blogs/scott_seely/archive/2009/07/17/calling-an-sta-com-object-from-a-wcf-operation.aspx) but my OperationContext.Current is always null. Any advice is much appreciated.
Service
Collection:
private static Dictionary<IAgent, COMAgent> agents = new Dictionary<IAgent, COMAgent>();
First action:
public void Login(LoginRequest request)
{
IAgent agent = OperationContext.Current.GetCallbackChannel<IAgent>();
lock (agents)
{
if (agents.ContainsKey(agent))
throw new FaultException("You are already logged in.");
else
{
ICOMClass startup = new ICOMClass();
string server = ConfigurationManager.AppSettings["Server"];
int port = Convert.ToInt32(ConfigurationManager.AppSettings["Port"]);
bool success = startup.Logon(server, port, request.Username, request.Password);
if (!success)
throw new FaultException<COMFault>(new COMFault { ErrorText = "Could not log in." });
COMAgent comAgent = new COMAgent { Connection = startup };
comAgent.SomeEvent += new EventHandler<COMEventArgs>(comAgent_COMEvent);
agents.Add(agent, comAgent);
}
}
}
Second Action:
public void Logoff()
{
IAgent agent = OperationContext.Current.GetCallbackChannel<IAgent>();
lock (agents)
{
COMAgent comAgent = agents[agent];
try
{
bool success = comAgent.Connection.Logoff();
if (!success)
throw new FaultException<COMFault>(new COMFault { ErrorText = "Could not log off." });
agents.Remove(agent);
}
catch (Exception exc)
{
throw new FaultException(exc.Message);
}
}
}
Take a look at this very similar post: http://www.netfxharmonics.com/2009/07/Accessing-WPF-Generated-Images-Via-WCF
You have to use an OperationContextScope to have access to the current OperationContext from the newly generated thread:
System.Threading.Thread thread = new System.Threading.Thread(new System.Threading.ThreadStart(delegate
{
using (System.ServiceModel.OperationContextScope scope = new System.ServiceModel.OperationContextScope(context))
{
result = InnerOperationInvoker.Invoke(instance, inputs, out staOutputs);
}
}));