How to write Unison code. Architecture (what exists) is in Architecture. This page is the spec: required patterns, folder layout, and known exceptions.
Agents: read this before adding a view, service, façade, or socket use case.
| Project | May reference | Must not |
|---|---|---|
Unison.Core |
Unison.Baileys |
XAML, WinRT, Unison.Uwp |
Unison.Socket |
Unison.Baileys |
WinRT, SQLite, Core ViewModels |
Unison.Baileys |
NuGet only | Other Unison projects |
Unison.Background |
Unison.Baileys |
Core, Socket, XAML |
Unison.Uwp |
All of the above | Protocol logic that belongs in Socket |
- Contracts and ViewModels live in Core.
- WinRT implementations live in UWP (
Services/,Data/,Transport/,UI/). - WhatsApp wire logic lives in Socket (
UseCases/,Session/, modules). ConnectionHandlernever calls a use case and never holds chats/messages.- Reconnect policy stays in the UWP host (
ConnectionUpdate).
| View / surface | ViewModel (Core) |
|---|---|
BootView |
ShellViewModel (boot navigation) |
StartView |
StartViewModel |
LoginView / LoginControl |
LoginViewModel |
MainView (AppShell) |
ShellViewModel |
ChatsView / ChatListView |
ChatListViewModel |
ChatDetailView |
ChatDetailViewModel |
| Chat info pane | ChatDetailInfoViewModel |
SettingsView / SettingsControl |
SettingsViewModel |
DebugView |
DebugViewModel |
ImageViewerView |
ImageViewerViewModel |
VideoViewerView |
VideoViewerViewModel |
NewChatDialog |
NewChatDialogViewModel |
- ViewModels inherit
Observable(Set/OnPropertyChanged/RaiseProperties) unless a dedicated INPC type already exists (ChatMessageViewModel). - Commands are
RelayCommand/RelayCommand<T>(ICommand). BindCommandin XAML. - ViewModels take interfaces in the constructor, never UWP types.
A row/bubble that can be tapped, downloaded, pinned, or played is not bound straight to the domain model.
| Item | ViewModel | Created by |
|---|---|---|
| Chat list row | ChatItemViewModel |
IChatItemVmFactory |
| Timeline bubble | ChatMessageViewModel |
IChatMessageVmFactory |
| Chat info (user/group) | ChatDetailInfoViewModel |
IChatDetailInfoViewModelFactory |
Do not new ChatMessageViewModel(...) from a view. Use the factory from DI.
Put in the ViewModel: state, commands, formatting, when to send, when to download, presence subscribe, dialog/picker/mic calls through contracts.
Allowed in code-behind:
InitializeComponent,OnNavigatedTo/From, system Back- Resolve VM from
App.Servicesand setDataContext Loaded→InitializeAsync;Unloaded→UninitializeAsync- Storyboards,
MediaElement/MediaPlayer,ScrollViewersnap - Control-to-control focus, visual states, pointer capture
Microsoft.Xaml.InteractivityEventTriggerBehavior→InvokeCommandAction
Forbidden in code-behind: calling Socket/Baileys, building send payloads, parsing protocol nodes, WhatsAppService.Instance, business policy (when to refresh names, when to mark read).
Prefer Behaviors over Click= handlers.
Root pages (*View : Page) host controls (*Control : UserControl) when the surface is large (Login, Settings, Debug). The page owns navigation chrome; the control owns the bound layout.
src/Unison.Uwp/UI/
Views/ pages and large surface controls
Controls/ reusable pieces (avatar, setting box, bubbles chrome, chat-info panes)
Dialogs/ ContentDialog XAML only
Converters/ IValueConverter
Templates/ DataTemplates (messages, chat items, preview kinds)
TemplateSelectors/ DataTemplateSelector
Helpers/ view-only helpers (rich text, presentation)
- Reusable visual →
Controls/. Do not paste a second copy into a page. - Dialog →
Dialogs/+ a method onIDialogService. Views do notnew ContentDialogfor product flows. - Converter →
Converters/. Do not put convert logic in the code-behind. - Themes →
Themes/Unison/andThemes/WhatsApp/(Theme.xaml,Styles.xaml,Controls.xaml). UseThemeResource, not hardcoded brushes that already exist. - Do not put ViewModels in UWP. They stay in
Unison.Core/ViewModels/.
IDialogService is the only entry. Methods that need form state take the target ViewModel (Imgur pattern):
Task ShowPairingCodeAsync(LoginViewModel loginVm, string code);
Task<string> ShowNewChatDialogAsync(NewChatDialogViewModel newChatVm);DialogService (UWP) constructs UI/Dialogs/* and sets DataContext. Catch the “single ContentDialog” COM error; do not crash.
Simple confirm/message/input may be inline ContentDialog inside DialogService, not inside a view.
Composition root: App.ConfigureServices in Unison.Uwp/App.xaml.cs.
| Kind | Lifetime |
|---|---|
Stores, façades, platform adapters, ShellViewModel |
Singleton |
Page ViewModels (Login, ChatList, ChatDetail, …) |
Transient |
| Collection item VMs | Factory (singleton factory, transient instances) |
Required:
- New service → interface in
Unison.Core/Contracts(WhatsApp domain →Contracts/WhatsApp/). - Implementation in UWP
Services/(WhatsApp →Services/WhatsApp/<Area>/as a Facade). - Register in
ConfigureServices. Constructor-inject everywhere else. - After adding a façade that listens to client events, resolve it once at startup (see Profile/History) so it does not miss the first events.
Forbidden:
- New
Foo.Instance/ service locator exceptApp.ServicesandApp.GetWhatsAppService()at the existing composition edges. - Registering leftover
MessageService/ContactService/ConnectionService/ProfileServiceunderServices/WhatsApp/(not façades). Those files are dead. WhatsAppService.Instancein new code. UseIWhatsAppServicefrom DI, and prefer the façade that owns the subject.
Existing singletons (SocketBrokerCoordinator.Instance, RuntimeDiagnosticsService.Instance, LiveTilesService.Instance) are legacy. New code takes the interface (ISocketBrokerService, IRuntimeDiagnostics, ILiveTilesService). Do not add more .Instance services.
Screens and ViewModels use:
IConnectionService, IMessageService, IChatService, IContactService, IProfileService, IHistoryService.
They do not subscribe to raw IWhatsAppService events. Those events are for façades only.
Need a live WhatsAppSession? IWhatsAppSessionProvider — do not cache the session across reconnects.
LoginViewModel talks only to IConnectionService.
Do not grow IWhatsAppService for UI features. Add the member on the façade that owns the subject.
SocketBridge implements IWhatsAppSocket so the client can keep working. New protocol features go on Socket use cases / modules, then a façade — not on IWhatsAppSocket unless the client still must see them.
- Route keys in
Unison.Core/Constants/NavigationRoutes.cs. - Page types only in
NavigatorService. Core never names a view type. - Auth boundaries:
NavigateAndClear(Boot / Start / Login / AppShell). No back stack into login. - Master-detail stays on
ChatsView(not a Frame push list → detail). - Settings keys and defaults:
LocalSettingsConstants. Do not invent a parallel key string; Background toasts that share a key must keep the same literal. - UI strings:
Strings/{tag}/Resources.resw+x:Uidand/orIStringResources. English fallback for missing keys. - Shipped languages:
en-US,pt-BR,es-ES,it-IT,nl-NL,id-ID,pl-PL. Add the key to all packs, or English-only with fallback — never a hardcoded sentence in a ViewModel. - Language packs stay in the main package (
AppxDefaultResourceQualifiers). Do not split resource packs. - Apply language before
InitializeComponent(already inAppctor).
- One operation per
UseCaseclass. No domain collections inside a use case. - File header: why it exists, what it does not do,
Ports: rc14 <ts path>. - Features register on
NodeDispatcher. Do not add a giant switch onConnectionHandler. - Host seams go in
Unison.Socket/Abstractions/(IWaTransport,ISocketLog, stores). UWP implements them. - Do not compile or “revive”
SocketClient.cs/PairingHandler.cs(on disk, out of the csproj). Live path isSocketBridge→WhatsAppSession. - Do not implement
TransferSocketToBrokerAsyncas a fake success. Until the session can lend the transport, it returnsfalse.
- Allman braces (opening brace on its own line).
sealedon UWPPage/UserControl/ façade classes.- Namespaces follow folders (
Unison.Uwp.UI.Views,Unison.Core.ViewModels,Unison.Socket.UseCases.Messages). - New comments and public remarks in English.
- Target Core/Socket at netstandard2.0. Do not use APIs that break that or .NET Native on ARM.
- UWP min version 10.0.16299 (Windows 10 Mobile). Prefer WinUI 2.7 controls already referenced (
Microsoft.UI.Xaml2.7.3). - Do not add NuGet packages unless the existing stack cannot do the job.
// BAD — protocol in a view
private async void Send_Click(...) { await WhatsApp.Socket.SendAsync(...); }
// GOOD — command on the VM, façade in the constructor
SendMessageCommand = new RelayCommand(async () => await _messages.SendTextMessageAsync(...), () => CanCompose);<!-- BAD -->
<Button Click="Send_Click" Content="Send"/>
<!-- GOOD -->
<Button Command="{Binding SendMessageCommand}" x:Uid="ChatDetail_Send"/>These are hybrid on purpose until Migration says otherwise:
WhatsAppServicestill owns in-memory chats, history body, send transport, persist.ChatDetailViewcode-behind still owns the message list, scroll,MediaElementchrome.ChatListViewmay still mirrorVisibleChatsduring initial-sync safe mode.SocketBridgebroker transfer / reclaim / cold restore return false / unused.ChatsModulein Socket is not instantiated by the bridge yet.- A few platform helpers still use
.Instance; new code uses the interface.
When touching these files, move a slice toward the spec (VM + façade), do not expand the hybrid.
- Architecture — layers and SocketBridge
- Application layer — façades, DI order, ViewModels
- UI and shell — navigation, themes, chat surface
- Socket stack — use cases and handler split
- Migration — remaining work