I have a Blazor Server App with the following DIV tags
<div class=mainScreen id="outerBox" style="width:#(TotalWidth)px;height:#(TotalHeight)px;">
foreach(Device thisDevice in MyDevices)
{
<div class=column id="#MainDiv" style="width:#(thisDevice.Width)px;height:#(thisDevice.Height)px;left:#thisDevice.XCoordinate;top:#thisDevice.YCoordinate">
Main Content Here...
</div>
}
</div>
I attempted to set the Height, Width, and X/Y coordinates using the code samples from this page - https://blazor.tips/blazor-how-to-ready-window-dimensions/ but that never worked and simply threw an uncaught exception no matter where I placed Try... blocks.
I then moved to a more straightforward JS call:
await Task.Run(async () =>
{
//Commenting out the OnInitializeAsync makes no difference but needs to be commented out when embedded
//On the main component
await this.OnInitializedAsync();
string data = await JSRuntime.InvokeAsync<string>("getMyWindow", new object[] { });
JObject offsets = (JObject)JsonConvert.DeserializeObject(data);
TotalHeight = offsets.Value<int>("height");
TotalHeight = offsets.Value<int>("width");
}
//In my JS file, the function looks as follows:
function getMyWindow() {
var obj = {};
obj.width = window.width;
obj.height = window.height;
return JSON.stringify(obj);
}
If I make this call directly in the code, nothing ever happens - even with the OnInitializeAsync commented out.
var result = SetDimensions().Result;
If I place this method in the OnAfterRendor method:
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
if (!SetTheDivs)
SetTheDivs = SetDimensions().Result;
StateHasChanged();
}
}
protected override void OnInitialized()
{
base.OnInitialized();
this.OnAfterRender(true);
}
everything hangs until I kill the project. There are never any errors but the code never runs when I place breakpoints on the height or width statements.
I even added in the Async version to no avail:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await SetDimensions();
StateHasChanged();
}
}
protected override async Task OnInitializedAsync()
{
await this.OnAfterRenderAsync(true);
}
Same result as everything hangs. I am at a complete loss as to how to proceed and I really could use some help!
As a point of clarity, it is the call to the JS that results in the hang:
string data = await JSRuntime.InvokeAsync<string>("getMyWindow", new object[] { });
I added in some alerts but they never run:
function getMyWindow() {
var obj = {};
alert("hi");
obj.width = screen.width;
obj.height = screen.height;
alert("ho");
return JSON.stringify(obj);
}
Thank you for your time!
BTW - I did change the double await to string data = JSRuntime.InvokeAsync<string>("getMyWindow", new object[] { }).Result;
UPDATE: I moved the JS call outside of the await altogether and I got the error:
InvalidOperationException: JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered. When prerendering is enabled, JavaScript interop calls can only be performed during the OnAfterRenderAsync lifecycle method.
In this case, I am literally calling the method from the OnAfterRenderAsync method:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnInitializedAsync();
if (firstRender)
{
await SetDimensions();
StateHasChanged();
}
}
Not sure what you want... Copy the code below and run it, and tell us if that is what you were trying to get.
Index.razor
#page "/"
<div class=mainScreen id="outerBox" style="width:#($"{TotalWidth}px");height:#($"{TotalHeight}px"); background-color: green; top:60px; position:absolute">
#foreach (Device device in devices)
{
<div class=column style="width:#($"{device.Width}px");height:#($"{device.Height}px");margin-left:#($"{device.Left}px");margin-top:#($"{device.Top}px"); background-color:aliceblue">
#device.Name: Main Content Here...
</div>
}
</div>
#code {
private int TotalWidth = 520;
private int TotalHeight = 530;
private IList<Device> devices = Enumerable.Range(1, 5).Select(i => new Device { Name = $"Name {i}", Width = 520, Height = 100, Left = 0, Top = 5 }).ToList();
public class Device
{
public string Name { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int Left { get; set; }
public int Top { get; set; }
}
}
Note: The OnInitialized{Async} pair of methods are the life-cycle methods of the base class ComponentBase. They are automatically called by the Blazor framework when a Razor component is being created. They are executed only once. You may override them, and add your logics, but you SHOULD never call them manually from your code.
This:
protected override async Task OnInitializedAsync()
{
await this.OnAfterRenderAsync(true);
}
This is wrong and must never be done. You should not call OnAfterRender{Async}. It is the Blazor framework that should call OnAfterRender{Async}, not the developer. Could you try to comprehend what your code is doing...
Try to understand that though the Razor components are defined as C# classes, they are special cases of Classes, that require special handling by the framework...
Update
Ken Tola, the following code I believe does what you're looking for. It reads the width and height of the window object, pass it to the Index component, and relocate your dear divs. Note that before the app relocates the divs, I check the values of the width and height, and determine the dimensions of the divs. This is of course is done for demonstration purposes, and you can manipulate those values as you wish...
Index.razor
#page "/"
#implements IDisposable
#inject IJSRuntime JSRuntime
<div class=mainScreen id="outerBox" style="width:#($" {TotalWidth}px");height:#($"{TotalHeight}px"); background-color: green; top:60px; position:absolute">
#foreach (Device device in devices)
{
<div class=column style="width:#($" {device.Width}px");height:#($"{device.Height}px");margin-left:#($"{device.Left}px");margin-top:#($"{device.Top}px"); background-color:aliceblue">
#device.Name: Main Content Here...
</div>
}
</div>
#code
{
private DotNetObjectReference<BrowserService> objRef;
private BrowserService BSS;
private int TotalWidth;
private int TotalHeight;
private IList<Device> devices = Enumerable.Range(1, 5).Select(i => new Device { Name = $"Name {i}", Width = 520, Height = 100, Left = 0, Top = 5 }).ToList();
public class Device
{
public string Name { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int Left { get; set; }
public int Top { get; set; }
}
protected override void OnInitialized()
{
BSS = new BrowserService();
objRef = DotNetObjectReference.Create(BSS);
BSS.Notify += OnNotify;
}
public void Dispose()
{
BSS.Notify -= OnNotify;
objRef?.Dispose();
}
public async Task OnNotify()
{
// Note that the notifier only notify your component
// that data is ready, and that the dimensions are read
// from a property. You can instead define event handler
// that pass the data in the form of EventArgs...
TotalWidth = BSS.Dimension.Width >= 877 ? 520 : 550;
TotalHeight = BSS.Dimension.Height >= 550 ? 800 : 1200;
await InvokeAsync(() => StateHasChanged());
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// This code is excuted only once, in order to initialize
// the JavaScript objects
if (firstRender)
{
await JSRuntime.InvokeAsync<object>
("myJsFunctions.getDimensions", objRef);
}
}
}
BrowserService.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.JSInterop;
public class BrowserService
{
public event Func<Task> Notify;
#nullable enable
public Dimension? Dimension { get; set; }
#nullable disable
[JSInvokableAttribute("GetDimensions")]
public async Task GetDimensions(string dimension)
{
JsonSerializerOptions options = new(JsonSerializerDefaults.Web)
{
WriteIndented = true
};
var _dimension = System.Text.Json.JsonSerializer.Deserialize(dimension, typeof(Dimension), options);
Dimension = (Dimension)_dimension;
if (Notify != null)
{
await Notify?.Invoke();
}
}
}
public class Dimension
{
public int Width { get; set; }
public int Height { get; set; }
}
}
Startup.ConfigureServices
services.AddScoped<BrowserService>();
_Host.cshtml
<script src="_framework/blazor.server.js"></script>
<script type="text/javascript">
window.myJsFunctions = {
getDimensions: function (dotnetHelper) {
var dimension = {
width: window.innerWidth,
height: window.innerHeight
};
var json = JSON.stringify(dimension);
return dotnetHelper.invokeMethodAsync('GetDimensions', json);
}
};
</script>
Note: Consider handling the relocation of the div elements when the window is resized. It should be responsive, right ? Not sure that in your case you can employ media query. Any how, as you can see, I have designed the code in such a way that it takes into account that your div elements may need to be relocate again and again, thus it constantly (when resizing) notifies your Index component of the changing dimensions. I guess this merits a new question.....
Related
I have had a hell of a time trying to get JS to play friendly with the Blazor lifecycle. I have a Blazor Server web app, using .NET 5. My website is heavy on images, and I use certain JS libraries to arrange images in a pretty way on the screen (masonry; lightboxes, etc.)
I have been really struggling with applying DOM-dependent JS. A lot of the JS code is meant to take DOM layout variability into account - for example, when an image loads, I have a JS script that is meant to get the width of the image as it is rendered on the screen (this varies, as it leans on css object-fit:scale-down) and then it resizes other DOM elements based on this calculation.
The problem is, I usually have to implement a loop (see SetWidth() in my code below) to apply and re-apply the JS script an arbitrary number of times so it can finally execute once the DOM finally finishes shifting around. This is ugly for obvious reasons, and it doesn't always work. I have pasted a sample of the approach I am taking below, just to illustrate my general approach. Can someone please tell me how to do this properly?
#inject IJSRuntime JSRuntime
#inject ResizeListener listener
<div class="text-overflow-dots" style="width:#($"{_widthLeader}px;")">
#ChildContent
</div>
#code {
[Parameter]
public string IdLeader { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
DotNetObjectReference<OverflowContainerDots> ObjectReference;
private int _widthLeader { get; set; } = 0;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// page lifecycle
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
ObjectReference = DotNetObjectReference.Create(this);
// Subscribe to the OnResized event. This will do work when the browser is resized.
listener.OnResized += WindowResized;
await SetWidth();
StateHasChanged();
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// DOM
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private async Task SetWidth()
{
do
{
await JSRuntime.InvokeVoidAsync("OverflowContainer.getWidthOfLeader", IdLeader, ObjectReference);
await Task.Delay(50);
} while (_widthLeader == 0);
}
// This method will be called when the window resizes.
// It is ONLY called when the user stops dragging the window's edge. (It is already throttled to protect your app from perf. nightmares)
private async void WindowResized(object _, BrowserWindowSize window)
{
await SetWidth();
StateHasChanged();
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// JS-invokable methods
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
[JSInvokable("UpdateWidth")]
public void UpdateDivWidth(int width)
{
if (_widthLeader != width)
{
_widthLeader = width;
StateHasChanged();
}
}
}
And my JS:
var OverflowContainer = OverflowContainer || {};
OverflowContainer.getWidthOfLeader = function (elementIdLeader, dotNetObject) {
var leader = document.getElementById(elementIdLeader);
if (leader === null)
return;
style = window.getComputedStyle(leader);
dotNetObject.invokeMethodAsync('UpdateWidth', parseInt(style.getPropertyValue('width')));
};
I'm developing an App with Xamarin.Android and I want to show a Toast notification while I'm exporting a report from JavaScript. My app calls the report and it's successfully generated. However, the Toast notification is never displayed. I discovered that only when I set a break point in that specific line it's shown in Visual Studio 2017.
This is part of my C# class for handling JS.
class CallJSInterface : Java.Lang.Object
{
private class Timetable
{
public string member { get; set; }
public string role { get; set; }
public string time { get; set; }
public string lastColor { get; set; }
}
private Context context;
public CallJSInterface(Context context)
{
this.context = context;
}
[Export]
[JavascriptInterface]
public void ExportToExcel(string results)
{
Toast.MakeText(context, context.GetString(Resource.String.LblExportMsg), ToastLength.Short).Show();
var timetable = JsonConvert.DeserializeObject<List<Timetable>>(results);
//Excel conversion
}
}
This is the value for the LblExportMsg in the Strings.xml:
<string name="LblExportMsg">Exporting your Agenda to Excel.</string>
Also, this is an example of I call the function in JS:
$("#linkDownload").click(function (e) {
e.preventDefault();
CSharp.ExportToExcel('[{"member":"Luis","role":"Timer","time":"00:15:15","lastColor":"red"},{"member":"Luis","role":"Timer 1","time":"00:15:00","lastColor":"green"},{"member":"Luis","role":"Timer 2","time":"00:15:17","lastColor":"red"},{"member":"Luis","role":"Timer 3","time":"00:07:15","lastColor":"green"},{"member":"Luis","role":"Timer 4","time":"00:23:15","lastColor":"red"},{"member":"Luis","role":"Timer 5","time":"00:15:15","lastColor":"green"},{"member":"Luis","role":"Timer 6","time":"01:15:15","lastColor":"yellow"},{"member":"Luis","role":"Timer 7","time":"00:18:15","lastColor":"green"},{"member":"Luis","role":"Timer 8","time":"00:15:22","lastColor":"green"}]');
});
Additionally, the HTML button:
<button type="button" id="linkDownload">Export</button>
Finally, this is how I add the JS interface to the WebView from the main activity:
webView.AddJavascriptInterface(new CallJSInterface(this), "CSharp");
Does anyone know what I am doing wrong? Is it something related to the context? How can I check it? Thanks for your help.
PS:
The minimum SDK is 21 and targets the SDK 27.
I'm using JSON.NET for deserializing.
I was able to fix it, I added a timeout before exporting the report and also, split in two the process one for displaying the Toast and another for the report.
$("#linkDownload").click(function (e) {
CSharp.Alert(currentTranslation.lblExportMsg);
setTimeout(function () {
CSharp.ExportToExcel(JSON.stringify(results));
}, 250);
});
I have designed a splash screen. The Java code is as below. In that screen, I have a button named "Do not show this screen again future". On pressing this button, the splash screen must never been shown in future, no matter how many times the app is started. How can I achieve this? Thanks in advance.
public class Qz1 extends Activity {
MyThread thread;
private class MyThread extends Thread
{
public boolean bRun = true;
#Override
public void run()
{
try
{
sleep(3200);
if (bRun)
{
startActivity(new Intent(getApplicationContext(), Qone.class));
Qz1.this.overridePendingTransition(R.anim.newright,
R.anim.newleft);
}
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_qz1);
thread = new MyThread();
thread.start();
}
public void round1(View v){
Intent i = new Intent(Qz1.this, Qone.class);
startActivity(i);
this.overridePendingTransition(R.anim.newright,
R.anim.newleft);
}
}
Use SharedPreferences for this.
You can save a persisted boolean value by calling
getPreferences(MODE_PRIVATE).edit().putBoolean("no_splash", true).commit();
Then you can check that value by calling
boolean noSplash = getPreferences(MODE_PRIVATE).getBoolean("no_splash", false);
If noSplash is true then launch your main Activity immediately rather than starting the Thread.
User shared Preferences to achieve this, create a class
public class Preference {
private SharedPreferences sharedPreferences;
private SharedPreferences.Editor editor;
public Preference(Context context) {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
}
public void writePreference(String key, Object value) {
if(value instanceof Boolean) {
editor = sharedPreferences.edit();
editor.putBoolean(key, (Boolean) value);
editor.commit();
}
}
public Object readPreference(String key , Object defValue) {
if(defValue instanceof Boolean)
return sharedPreferences.getBoolean(key, (Boolean) defValue);
else
return null;
}
public Boolean getDisableSplash() {
return (Boolean) readPreference("disable", false);
}
public void disableSplash(Boolean value)) {
writePreference("disable", valve);
}
}
and in your main create an object of Preference to read and write preference
Preference preference = new Preference(YourActivity.this);
Boolean result = preference.getDisableSplash();
if(!result) {
// dissable you splash activity here and move to next one
}
and when you want to disable it simply
Preference preference = new Preference(YourActivity.this);
preference.disableSplash(true);
You can solve this by creating delegate activity, make an empty activity an set it as launcher activity,
On the delegate activity oncreate check your preference if you should show the splash finish the delegate activity and show it else show your home screen.
First of all, I'm working with some specific API ( Grand Stream GXV3275 phone ) which requires that Intent - BroadcastReceiver combo breaker.
When my device is on landscape orientation it works good so the problem came with Intent - BroadcastReceiver.
So I need that IntentFilter to know my HOOKEVENT ans then receive it with that BroadcastReceiver.
I just want to know why it doesn't even show the alert or don't work at all.
Is that possible to deal with IntentFilter on CordovaPlugin? With BroadcastReceiver?
I made some test on my CordovaActivity and HOOKEVENT ; updating a text-view.
So I assume that's a problem with CordovaPlugin.
I also tried to do:
CordovaActivity activity = (CordovaActivity) this.cordova.getActivity();
activity.getJs();
Which normally allow me to get string that works on my activity but gave me NPE..
public class Toast extends CordovaPlugin {
private String javascript = "";
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
initHookEvent();
switch (action) {
case "reversed":
reversedTest();
return true;
}
return false;
}
private Activity getActivity() { return this.cordova.getActivity();}
private void reversedTest(){
Configuration configuration = getActivity().getResources().getConfiguration();
if(configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){
webView.sendJavascript("javascript:document.getElementById(\"combi\").innerHTML=\"Landscape\";");
}
webView.sendJavascript(javascript);
}
public void initHookEvent() {
IntentFilter filter = new IntentFilter("com.base.module.phone.HOOKEVENT");
getActivity().registerReceiver(broadcastReceiver, filter);
}
public BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
webView.sendJavascript("javascript:alert(\"test\");");
if (intent.getBooleanExtra("hookoff", false)){
javascript = "javascript:document.getElementById(\"combi\").innerHTML=\"decroche\";";
}
else{
javascript = "javascript:document.getElementById(\"combi\").innerHTML=\"raccroche\";";
}
}
};
I found myself my problem.
I create a specific plugin only for that after.
You just needed to :
webView.sendJavascript("javascript:document.getElementById(\"combi\").innerHTML=\"decroche\";");
And
getActivity().getApplicationContext().registerReceiver(broadcastReceiver_hook, filter_hook);
Here's my final plugin :
public class Hook extends CordovaPlugin {
#Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
initHookEvent();
return false;
}
/**
* Use to get the current Cordova Activity
* #return your Cordova activity
*/
private Activity getActivity() { return this.cordova.getActivity();}
/**
* Initializing GXV 3275 Hook Event
* You ABSOLUTELY need to precise getActivity().getApplicationContext()
* before registerReceiver() otherwise it won't get the good context.
*/
public void initHookEvent() {
IntentFilter filter_hook = new IntentFilter("com.base.module.phone.HOOKEVENT");
getActivity().getApplicationContext().registerReceiver(broadcastReceiver_hook, filter_hook);
}
/**
* BroadcastReceiver is also needed with GXV 3275 Hook Event
* Just sendJavascript for each cases
* /!\ webView /!\
* Is natively created by extending CordovaPlugin
*/
public BroadcastReceiver broadcastReceiver_hook = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
if ( intent.getBooleanExtra("hookoff", false)){
webView.sendJavascript("javascript:document.getElementById(\"combi\").innerHTML=\"decroche\";");
webView.sendJavascript("javascript:document.getElementById(\"combi\").style.opacity = 1;");
}
else{
webView.sendJavascript("javascript:document.getElementById(\"combi\").innerHTML=\"raccroche\";");
webView.sendJavascript("javascript:document.getElementById(\"combi\").style.opacity = 1;");
}
}
};
}
I'm starting out with SignalR and I have a situation where I'm going to have a SignalR site that will be broadcasting messages to clients, but I also need an admin interface that will actually trigger those messages. The admin page will call server side methods that will, in turn, call client side Javascript methods for regular users. So I'm thinking I can either set up two separate hubs (one for admin, one for everybody else) or I can have methods in a single hub that can only be called by the admin that will check authorization.
But in addition to the authorization, I'd like to have SignalR not include admin methods or an admin hub in the generated Javascript proxy classes so that I'm not advertising their existence (again - this is NOT the only security, I will be checking authorization). Is there an attribute or property I can set on individual hubs or on methods within a hub that will suppress them from being included in the proxy (but still have them callable from Javascript)? I know you can set EnableJavaScriptProxies to false in your HubConfiguration, but that seems to be global and I'd like to keep the proxy for the stuff I do want the regular client to be using.
There is one trick using interfaces. As proxy will generate only public methods in proxy, you can create hub using interface like this:
public class MyHub : Hub, IMyHub
{
void IMyHub.NotGeneratedOnClient()
{
}
public void GeneratedOnClient()
{
}
}
NotGeneratedOnClient method will not be visible if you use object of type MyHub, you can access it only using interface. As method is not public proxy generator is not going to add it to client proxy
We don't have a way of excluding specific methods from the proxy today. You'd have to re-implement your own proxy generator that basically does what we do in our default impl but has knowledge of some attribute to skip generation of specific methods.
We can conceivable add this in a future version of SignalR. File an issue on github if you feel strongly about having this.
Here's the default implementation (it would have been easier if we made more methods virtual and non static).
https://github.com/SignalR/SignalR/blob/master/src/Microsoft.AspNet.SignalR.Core/Hubs/DefaultJavaScriptProxyGenerator.cs
Here is a modified DefaultJavaScriptProxyGenerator with the following changes:
It will exclude functions from Javascript proxy generation with a new [HubMethodExcludeFromProxy] attribute.
The private static functions have changed to protected virtual for future derivatives.
The GenerateProxy( ) function has an overload to include DocComments, but that was not caching the results like the non DocComments version. Now they both cache.
Two resources, Resources.DynamicComment_CallsMethodOnServerSideDeferredPromise and Resources.DynamicComment_ServerSideTypeIs were private to another assembly, so to get things to compile, I copied the text from the resource file directly. These two resources are only used if DocComments is true.
All of the DefaultJavaScriptProxyGenerator references were changed to CustomJavaScriptProxyGenerator except for one, which is used to locate the resource script Microsoft.AspNet.SignalR.Scripts.hubs.js, located in a different assembly.
First, you will need to update the dependency resolver to use the new CustomJavaScriptProxyGenerator for the IJavaScriptProxyGenerator interface. If you are using the default resolver, you can set up a custom resolver like this:
map.RunSignalR(
new HubConfiguration() {
Resolver = new CustomDependencyResolver()
}
);
And here is a custom resolver that derives from the DefaultDependecyResolver:
namespace Microsoft.AspNet.SignalR
{
public class CustomDependencyResolver : DefaultDependencyResolver
{
MyDependencyResolver() : base()
{
var proxyGenerator = new Lazy(() => new CustomJavaScriptProxyGenerator(this));
Register(typeof(IJavaScriptProxyGenerator), () => proxyGenerator.Value);
}
}
}
And finally, here is the new CustomJavaScriptProxyGenerator.cs file (the HubMethodExcludeFromProxyAttribute class is at the bottom):
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
// Mods by Brain2000
using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using Microsoft.AspNet.SignalR.Json;
using Microsoft.AspNet.SignalR.Hubs;
using Newtonsoft.Json;
namespace Microsoft.AspNet.SignalR.Hubs
{
public class CustomJavaScriptProxyGenerator : IJavaScriptProxyGenerator
{
protected static readonly Lazy _templateFromResource = new Lazy(GetTemplateFromResource);
protected static readonly Type[] _numberTypes = new[] { typeof(byte), typeof(short), typeof(int), typeof(long), typeof(float), typeof(decimal), typeof(double) };
protected static readonly Type[] _dateTypes = new[] { typeof(DateTime), typeof(DateTimeOffset) };
protected const string ScriptResource = "Microsoft.AspNet.SignalR.Scripts.hubs.js";
protected readonly IHubManager _manager;
protected readonly IJavaScriptMinifier _javaScriptMinifier;
protected readonly Lazy _generatedTemplate;
protected readonly Lazy _generatedTemplateWithComments;
public CustomJavaScriptProxyGenerator(IDependencyResolver resolver) :
this(resolver.Resolve(),
resolver.Resolve())
{
}
public CustomJavaScriptProxyGenerator(IHubManager manager, IJavaScriptMinifier javaScriptMinifier)
{
_manager = manager;
_javaScriptMinifier = javaScriptMinifier ?? NullJavaScriptMinifier.Instance;
_generatedTemplate = new Lazy(() => GenerateProxy(_manager, _javaScriptMinifier, includeDocComments: false));
_generatedTemplateWithComments = new Lazy(() => GenerateProxy(_manager, _javaScriptMinifier, includeDocComments: true));
}
public string GenerateProxy(string serviceUrl)
{
serviceUrl = JavaScriptEncode(serviceUrl);
return _generatedTemplate.Value.Replace("{serviceUrl}", serviceUrl);
}
public string GenerateProxy(string serviceUrl, bool includeDocComments)
{
if (!includeDocComments) return GenerateProxy(serviceUrl); //use the includeDocComments: false cached version
serviceUrl = JavaScriptEncode(serviceUrl);
return _generatedTemplateWithComments.Value.Replace("{serviceUrl}", serviceUrl);
}
protected virtual string GenerateProxy(IHubManager hubManager, IJavaScriptMinifier javaScriptMinifier, bool includeDocComments)
{
string script = _templateFromResource.Value;
var hubs = new StringBuilder();
var first = true;
foreach (var descriptor in hubManager.GetHubs().OrderBy(h => h.Name))
{
if (!first)
{
hubs.AppendLine(";");
hubs.AppendLine();
hubs.Append(" ");
}
GenerateType(hubManager, hubs, descriptor, includeDocComments);
first = false;
}
if (hubs.Length > 0)
{
hubs.Append(";");
}
script = script.Replace("/*hubs*/", hubs.ToString());
return javaScriptMinifier.Minify(script);
}
protected virtual void GenerateType(IHubManager hubManager, StringBuilder sb, HubDescriptor descriptor, bool includeDocComments)
{
// Get only actions with minimum number of parameters.
var methods = GetMethods(hubManager, descriptor);
var hubName = GetDescriptorName(descriptor);
sb.AppendFormat(" proxies['{0}'] = this.createHubProxy('{1}'); ", hubName, hubName).AppendLine();
sb.AppendFormat(" proxies['{0}'].client = {{ }};", hubName).AppendLine();
sb.AppendFormat(" proxies['{0}'].server = {{", hubName);
bool first = true;
foreach (var method in methods)
{
if (!first)
{
sb.Append(",").AppendLine();
}
GenerateMethod(sb, method, includeDocComments, hubName);
first = false;
}
sb.AppendLine();
sb.Append(" }");
}
protected virtual string GetDescriptorName(Descriptor descriptor)
{
if (descriptor == null)
{
throw new ArgumentNullException("descriptor");
}
string name = descriptor.Name;
// If the name was not specified then do not camel case
if (!descriptor.NameSpecified)
{
name = JsonUtility.CamelCase(name);
}
return name;
}
protected virtual IEnumerable GetMethods(IHubManager manager, HubDescriptor descriptor)
{
return from method in manager.GetHubMethods(descriptor.Name).Where(md => md.Attributes.FirstOrDefault(a => (a.GetType() == typeof(HubMethodExcludeFromProxyAttribute))) == null)
group method by method.Name into overloads
let oload = (from overload in overloads
orderby overload.Parameters.Count
select overload).FirstOrDefault()
orderby oload.Name
select oload;
}
protected virtual void GenerateMethod(StringBuilder sb, MethodDescriptor method, bool includeDocComments, string hubName)
{
var parameterNames = method.Parameters.Select(p => p.Name).ToList();
sb.AppendLine();
sb.AppendFormat(" {0}: function ({1}) {{", GetDescriptorName(method), Commas(parameterNames)).AppendLine();
if (includeDocComments)
{
sb.AppendFormat(" /// Calls the {0} method on the server-side {1} hub.\nReturns a jQuery.Deferred() promise.", method.Name, method.Hub.Name).AppendLine();
var parameterDoc = method.Parameters.Select(p => String.Format(CultureInfo.CurrentCulture, " /// Server side type is {2}", p.Name, MapToJavaScriptType(p.ParameterType), p.ParameterType)).ToList();
if (parameterDoc.Any())
{
sb.AppendLine(String.Join(Environment.NewLine, parameterDoc));
}
}
sb.AppendFormat(" return proxies['{0}'].invoke.apply(proxies['{0}'], $.merge([\"{1}\"], $.makeArray(arguments)));", hubName, method.Name).AppendLine();
sb.Append(" }");
}
protected virtual string MapToJavaScriptType(Type type)
{
if (!type.IsPrimitive && !(type == typeof(string)))
{
return "Object";
}
if (type == typeof(string))
{
return "String";
}
if (_numberTypes.Contains(type))
{
return "Number";
}
if (typeof(IEnumerable).IsAssignableFrom(type))
{
return "Array";
}
if (_dateTypes.Contains(type))
{
return "Date";
}
return String.Empty;
}
protected virtual string Commas(IEnumerable values)
{
return Commas(values, v => v);
}
protected virtual string Commas(IEnumerable values, Func selector)
{
return String.Join(", ", values.Select(selector));
}
protected static string GetTemplateFromResource()
{
//this must remain "DefaultJavaScriptProxyGenerator" because the resource "Microsoft.AspNet.SignalR.Scripts.hubs.js" lives there
using (Stream resourceStream = typeof(DefaultJavaScriptProxyGenerator).Assembly.GetManifestResourceStream(ScriptResource))
{
var reader = new StreamReader(resourceStream);
return reader.ReadToEnd();
}
}
protected virtual string JavaScriptEncode(string value)
{
value = JsonConvert.SerializeObject(value);
// Remove the quotes
return value.Substring(1, value.Length - 2);
}
}
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
public sealed class HubMethodExcludeFromProxyAttribute : Attribute
{
}
}
Now all you need to do is all a decorator to your hub methods, such as:
public class MyHub : Hub
{
[HubMethodExcludeFromProxy]
public void NotGeneratedOnClient()
{
}
public void GeneratedOnClient()
{
}
}
EDIT : There is an issue with dependency injection where if you have two different instances of a resolver, one in the GlobalHost.DependencyResolver and one in the Signalr configuration, it will cause remote methods to sometimes not work. Here is the fix:
//use only !ONE! instance of the resolver, or remote SignalR functions may not run!
var resolver = new CustomDependencyResolver();
GlobalHost.Configuration.DependencyResolver = resolver;
map.RunSignalR(
new HubConfiguration() {
Resolver = resolver;
}
);
Reference: https://github.com/SignalR/SignalR/issues/2807