Putting it together
A shared notes list. Click the button, the server picks the text and everyone currently
viewing this page sees it appear, live, without refreshing. Open this page in a second tab
to check it: add a note in one, watch it appear in the other, even though only one tab
clicked anything.
Server
// Services/Notes/AppData.cs
public class AppData
{
public List<Note> Notes { get; set; }
}
public class Note
{
public int Id { get; set; }
public string Text { get; set; }
}
// Services/Notes/Controller.cs
public static class Controller
{
public static string UpdateNotesId { get; } = "notes";
private static readonly string[] SampleNotes =
{
"Bindary is neat",
"Hello from the docs",
"This is a shared note",
"Testing the live update",
"Notes stay short",
"Pushed, not polled",
"Nice to see you here",
"Works across tabs",
"Straight from the server",
"No refresh needed"
};
private static List<Note> _notes = new List<Note>();
private static int _nextId = 1;
public static void RegisterActions()
{
DataUpdateManager.AddUpdateAction(UpdateNotesId, SendNotes);
}
// called once, when the demo section first renders
public static async Task Subscribe(Client client, DataRequest request)
{
DataUpdateManager.Subscribe(UpdateNotesId, client);
await SendNotes(client);
}
// called from the "Add note" button. The client never supplies the text,
// the server always picks it, so there's nothing here for a request built
// by hand, dev console or otherwise, to actually control.
public static async Task AddNote(Client client, DataRequest request)
{
var random = new Random();
var text = SampleNotes[random.Next(SampleNotes.Length)];
lock (_notes)
{
_notes.Add(new Note { Id = _nextId++, Text = text });
// cap growth
if (_notes.Count > 10)
_notes.RemoveAt(0);
}
// ack, so dataSendAsync resolves
var ack = new DataResponse(request.CallbackId);
await Server.Send(client, ack, request.XhrRequestId);
// push to every subscribed client, including this one
await DataUpdateManager.Update(updateId: UpdateNotesId);
}
private static async Task SendNotes(
Client client,
object subscriptionArgs = null,
object updateArgs = null)
{
var response = new DataResponse<AppData>();
var rule = new DataRules
{ Update = true,
UpdateKey = "id",
Path = "demo.backendDemo.notes"
};
response.Data.AppData = new AppData { Notes = _notes };
response.Data.AppDataRules = new CaseInsensitiveDictionary<DataRules>();
response.Data.AppDataRules["Notes"] = rule;
await Server.Send(client, response);
}
}
The server picks the text, not the client. Nothing sent in the request is ever read for
this, so there's no way, dev console included, to make this list say anything the server
didn't already approve.
RegisterActions() needs one call in Startup.cs, alongside your
other startup wiring: Notes.Controller.RegisterActions();.
DataUpdateManager.Update(UpdateNotesId) won't compile as written, a single
string argument matches two different overloads equally well, updateId on one,
groupId on the other. Name the argument, Update(updateId: UpdateNotesId),
to resolve it to the one you actually mean.
Client
ctrl.subscribeNotes = async () =>
{
await $bindary.dataSendAsync(null, 'Notes.Controller.Subscribe');
};
ctrl.addNote = async () =>
{
await $bindary.dataSendAsync(null, 'Notes.Controller.AddNote');
};
<div b-post-render="subscribeNotes">
<ul>
<li b-repeat="demo.backendDemo.notes" b-value="text"></li>
</ul>
<a ui-type="Button" ui-text="Add note"
ui-command="$bindary.controller.addNote"></a>
</div>
demo.backendDemo.notes has to already exist client-side before this renders.
b-repeat resolves its data-key the moment this markup renders, before
subscribeNotes even runs, let alone before its response comes back, so
there's nothing that would create it in time on its own. It's seeded once in the shared
load hook, the same place every other reference page's demo data on this site
comes from.
Result
Every push here sends the whole list, all ten notes, not just the new one.
UpdateKey = "id" is what makes that safe: a matching id gets updated in place,
a new id gets appended, so resending the full list on every push doesn't duplicate anything
already there. Path is why this lands at demo.backendDemo.notes
specifically, rather than merging directly onto AppData's own root.
Sending everything every time is a deliberate simplification for this demo, not the only
way to use this. Sending only what's genuinely new, tracking per-subscriber what a client
has already seen, the args object on a subscription is where that would live,
means less to send as a list grows, with UpdateKey then acting as a safety net
against an accidental duplicate rather than doing the main work. Worth reaching for once
"resend everything" stops being cheap enough.
UpdateKey is matched against the property name as it arrives client-side, not
the C# property name. With camelCase serialization, that's "id", not
"Id". Get this wrong and every comparison quietly reads undefined
on both sides, which counts as a match, so the merge always updates whichever item happens
to be first, no error, nothing obviously broken, just the wrong item overwritten every time.