Skip to main content

Chat Sample

The Chat sample demonstrates how to create a minimal chat application. Its main scope is to demonstrates the real time relationships between the Entities. It also demonstrates the creation of an interactive console application. For the Desktop app we use the Avalonia UI framework.

Download or clone the samples from GitHub samples repository.

Quick Start

  1. Open Chat.sln solution in Visual Studio

  2. Build the solution

  3. Run or Debug the ChatServer

  4. Run or Debug an instance of ChatDesktopApp

To login use one of the pre-configured users that are in the app.config of the server (those accounts are there only for initialization as they are stored in db). You can use the same user in more than one instances. Note: the user credentials are hardcoded to the Desktop App for simplicity.

Then create a new Room named CognibaseFuns.

  1. Run or Debug an instance of ChatConsoleApp

To login use one of the preconfigured users that are in the app.config of the server (those accounts are there only for initialization as they are stored in db). You can use the same user in more than one console instances. Use the below:

  • Domain FQDN (YourMachineName): Basic
  • Domain (BASIC): Basic
  • User Name (Yourusername) : user1 // or user2, ...
  • Role Name (): // click enter, just leave it empty
  • Password (): user1 // or user2, ...
  • Save login (yes/no) ? (no): y

Then issue a join room command, eg /join CognibaseFuns .

  1. Now you are ready to exchange chat messages with other peers of your room either using the Desktop or Console app.

Chat Codebase

The sample consists of the following projects:

  • ChatDomain: The Domain (Entity Model) of the application.
  • ChatServer: The Cognibase Object Server.
  • ChatDesktopApp: The Desktop client application.
  • ChatConsoleApp: The Command line / Console client application.

Domain

In the Chat Domain project there are two folders:

  • Entities: Where the entities reside. There are 3 Entities
  • System: Where two classes exits that make this assembly be treated as Domain from Cognibase (see more here)

The Chat Domain contains the following Entities.

User

Represents a user of the Chat.

[PersistedClass]
public class User : DataItem
{
[PersistedProperty(IdOrder = 1)]
public string Username { get => getter<string>(); set => setter(value); }

[PersistedProperty]
public DateTime LastLoginTime { get => getter<DateTime>(); set => setter(value); }

[PersistedProperty(ReverseRef = nameof(ChatMessage.Author))]
public DataItemRefList<ChatMessage> Messages => getList<ChatMessage>();

[PersistedProperty(ReverseRef = nameof(ChatRoom.Users))]
public DataItemRefList<ChatRoom> ChatRooms => getList<ChatRoom>();
}

Chat Room

Represents a chat room where users can communicate.

[PersistedClass]
public class ChatRoom : DataItem
{
[PersistedProperty(IdOrder = 1, AutoValue = AutoValue.Identity)]
public long Id { get => getter<long>(); set => setter(value); }

[PersistedProperty(IsMandatory = true)]
public string Name { get => getter<string>(); set => setter(value); }

[PersistedProperty]
public DataItemRefList<ChatMessage> Messages => getList<ChatMessage>();

[PersistedProperty(ReverseRef = nameof(User.ChatRooms))]
public DataItemRefList<User> Users => getList<User>();
}

Message

Represents a message sent by an Author in the chatroom.

[PersistedClass]
public class ChatMessage : DataItem
{
[PersistedProperty(IdOrder = 1, AutoValue = AutoValue.Identity)]
public long Id { get => getter<long>(); set => setter(value); }

[PersistedProperty]
public string Text { get => getter<string>(); set => setter(value); }

[PersistedProperty]
public DateTime CreatedTime { get => getter<DateTime>(); set => setter(value); }

[PersistedProperty]
public DateTime FirstReadTime { get => getter<DateTime>(); set => setter(value); }

[PersistedProperty(ReverseRef = nameof(User.Messages))]
public User Author { get => getter<User>(); set => setter(value); }
}

Chat Server

The Chat Server project uses the (default) SQLite adapter and references the Chat Domain. It also contains a few lines to bootstrap the Cognibase Object Server.

Below is the Program.cs:

[MTAThread]
private static void Main(string[] args)
{
// enable Serilog
LogManager.RegisterAgentType<SerilogAgent>();

// start the object server
ServerManager.StartInConsole();
}

Also in the app.config file there is the the definition of the Chat domain in the DataStore element.

Chat Desktop App

The Chat Desktop app contains the business logic and the user interface. It uses the Avalonia UI framework. The user can see the joined ChatRooms, select one and exchange messages. The user can also create/join ChatRooms as well as leave an already joined ChatRoom.

During startup the login popup appears so as the Cognibase Client Object Manager will connect to the Object Server. The login username/password are hardcoded for quick testing (but you can omit the hardcoded credentials).

The bootstrap code is written in the MainWindow.axaml.cs class. During the opening of the Window, it uses the component AvaloniaStartupHelper that provides ready to use components for login and initialization. This component shows the standard Avalonia login popup and performs the login. It also offers two delegates, a Quit action delegate that is used normally to terminate the app and a load action that is used to execute after login to perform the initial reading of the Chat user that represents the current logged in account.

protected override async void OnOpened(EventArgs e)
{
base.OnOpened(e);

// build the startup helper that contains the auth manager, the auth dialog and the loader
_startupHelper = new AvaloniaStartupHelper(this, App.Client);
_startupHelper.AuthVm = new SimpleAuthDialogVm { DomainFullName = "Basic", Username = "user1", Password = "user1" };
_startupHelper.QuitAction = () => Close(); // set the quit action
_startupHelper.DataLoadAction = async () =>
{
// data initialization flow
// get username
var myusername = App.Client.PrimaryServerMgr.ClientConnectionInfo.ClientIdentity.UserName;

// get or create user
_currentUser = DataItem.GetOrCreateDataItem<User>(App.Client, new object[] { myusername });
_currentUser.LastLoginTime = DateTime.Now;

// try save or else retry
if (!App.Client.Save(_currentUser).WasSuccessfull)
{
// log
await _dialog.ShowError("Error", $"Error logging to chat app. User {myusername} is not registered!");
_currentUser.Dispose();
_currentUser = null;
return;
}

// set data source in main Avalonia thread
Dispatcher.UIThread.Invoke(() =>
{
// set collection in your
_vm.CurrentUser = _currentUser;
mainView.DataContext = _vm;
});
};

// show the authentication dialog
await _startupHelper.ShowAuthDialog().ConfigureAwait(false);
}

In the main view we have the following XAML.

<DockPanel>
<Grid DockPanel.Dock="Left" RowDefinitions="80,*" Width="200">
<StackPanel Orientation="Vertical" Grid.Row="0">
<TextBox Margin="5" Width="190"
HorizontalAlignment="Stretch"
Text="{Binding NewRoomText}" />
<Button Width="190" Margin="5"
HorizontalAlignment="Right"
HorizontalContentAlignment="Center"
Command="{Binding CreateRoomCommand}">
Create/Join Room
</Button>
</StackPanel>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Visible">
<ListBox ItemsSource="{Binding CurrentUser.ChatRooms}" SelectedItem="{Binding SelectedChatRoom}">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="*, 50">
<TextBlock Margin="4"
Text="{Binding Name}" />
<Button Grid.Column="1" Background="Transparent"
Command="{Binding $parent[ItemsControl].DataContext.LeaveRoomCommand}"
CommandParameter="{Binding}" HorizontalAlignment="Left"
HorizontalContentAlignment="Left" ToolTip.Tip="Leave Room">
x
</Button>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</Grid>
<Grid RowDefinitions="*,100">
<ScrollViewer VerticalScrollBarVisibility="Visible">
<ListBox ItemsSource="{Binding SelectedChatRoom.Messages}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Margin="4"
Text="{Binding Author.Username}" />
<TextBlock Margin="4">: </TextBlock>
<TextBlock Margin="4"
Text="{Binding Text}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
<Grid Grid.Row="1" ColumnDefinitions="*,100">
<TextBox AcceptsReturn="True" Margin="5"
Text="{Binding NewMessageText}"
Watermark="Enter your text to send and click on Send button" />
<Button Grid.Column="1" Margin="5"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
Command="{Binding SendMessageCommand}">
Send
</Button>
</Grid>
</Grid>
</DockPanel>

In the main view model MainViewModel.cs we have the following code to handle the commands.

private async Task CreateRoom()
{
if(!String.IsNullOrWhiteSpace(NewRoomText))
{
// response
ClientTxnInfo response = null;

// search for this room
SearchArg args = new SearchArg(nameof(ChatRoom.Name), NewRoomText);
var curRoom = _client.FindDataItem<ChatRoom>(args);

// if room no found
if (curRoom == null)
{
// create and save the room
curRoom = _client.CreateDataItem<ChatRoom>();
curRoom.Name = NewRoomText;
curRoom.Users.Add(CurrentUser); // the reverse reference will be automtically created

// save
response = await _client.SaveAsync(false, TxnAutoInclusion.References, curRoom);

}
else if (!curRoom.Users.Contains(CurrentUser)) // if room not contains user add the user to the room
{
// add
curRoom.Users.Add(CurrentUser);

// save
response = await _client.SaveAsync(false, TxnAutoInclusion.References, curRoom);
}

// if success close form
if (response != null && !response.WasSuccessfull)
{
// reset items
_client.ResetAllMonitoredItems();

// notify user
await _dialogService.ShowError("Error", "Could not save data. Try again or cancel edit.");
}
else
{
// after save reset the textbox
NewRoomText = null;
}
}
}

private async Task LeaveRoom(ChatRoom item)
{
// check the item is not null
if (item != null)
{
// confirm deletion
AsyncDialogResult result = await _dialogService.AskConfirmation($"Leave Room {item.Name}?",
$"Do you want to Proceed?");
if (result == AsyncDialogResult.NotConfirmed)
return;

// cache selected
var curSelected = SelectedChatRoom;

// remove user
_currentUser.ChatRooms.Remove(item);

// save
ClientTxnInfo saveResult = await _client.SaveAsync(item, _currentUser);

// if not success unmark and notify user for the failure
if (!saveResult.WasSuccessfull)
{
// reset items
_client.ResetAllMonitoredItems();

// notify
await _dialogService.ShowError("Error", $"Could not leave room {item.Name}.");
}

// check if it was selected and set as selected the first found
if(curSelected == item)
SelectedChatRoom = _currentUser.ChatRooms.FirstOrDefault();
}
}

private async Task SendMessage()
{
if (SelectedChatRoom != null && !String.IsNullOrEmpty(NewMessageText))
{
// create a message and save it
var newMsg = _client.CreateDataItem<ChatMessage>();
newMsg.Text = NewMessageText;
newMsg.CreatedTime = DateTime.Now;
SelectedChatRoom.Messages.Add(newMsg);
CurrentUser.Messages.Add(newMsg); // set the author - reverse reference is automatically added

// save
var SaveResult = await _client.SaveAsync(false, TxnAutoInclusion.References, newMsg, CurrentUser, SelectedChatRoom);

// if success close form
if (!SaveResult.WasSuccessfull)
{
// reset items
_client.ResetAllMonitoredItems();

// notify user
await _dialogService.ShowError("Error", "Could not save data. Try again or cancel edit.");
}
else
{
// after save reset the textbox
NewMessageText = null;
}
}
}

Chat Console App

The Chat console app is a console application that starts a Cognibase Client Object Manager and then the user can join one room and exchange messages in this room. The user can also leave a ChatRoom.

The Initialization of the connection to the Object Server occurs in the bootstrap Program.cs class.

static void Main(string[] args)
{
// enable serilog
LogManager.RegisterAgentType<SerilogAgent>();

//
// SETTINGS SETUP
//

// Read client settings
var settings = ConfigBuilder.Create().FromAppConfigFile();

// Get proper SECTION
var clientSettings = settings.GetSection<ClientSetupSettings>();

//
// APPLICATION SETUP
//

// Initialize the correct (Client) Cognibase Application through the Application Manager
var cApp = ApplicationManager.InitializeAsMainApplication(new ClientApplication());

// Initializes a Client Object Manager with the settings from configuration
var client = ClientObjMgr.Initialize(cApp, ref clientSettings);

// Registers domains through Domain Factory classes that reside in Domain assembly
client.RegisterDomainFactory<IdentityFactory>();
client.RegisterDomainFactory<DomainFactory>();

// log
Console.WriteLine("Starting application...");

//
// SECURITY SETUP
//

// Initialize Security PROFILE
cApp.InitializeApplicationSecurity(client, ref clientSettings);

// set the chat controller
_controller = new ChatController(cApp);

//
// RUN
//
cApp.StartUpClient(StartupConnectionMode.ConnectAndStart);
}

The ChatController class is the class that performs the business logic and handles the interactive commands.

Initially it handles the connection events to load the current user and their ChatRooms. Then in the main loop it reads the Console and handles the join/leave commands and the new text messages.

private async Task MainLoop()
{
// set console in read mode
ConsoleManager.SetConsoleReadMode("");

// loop to perfor the actual pinging
while (!_isAborted)
{
// read current line
var line = ConsoleManager.ReadConsoleLine(_prompt, false, true);

// check if it is a join command
if (line.StartsWith("/join ", StringComparison.OrdinalIgnoreCase))
{
// get the room name
var roomName = line.Replace("/join", "", StringComparison.OrdinalIgnoreCase).Trim();

// search for this room
SearchArg args = new SearchArg(nameof(ChatRoom.Name), roomName);
var curRoom = App.Client.FindDataItem<ChatRoom>(args);

// if room no found
if (curRoom == null)
{
// create and save the room
curRoom = App.Client.CreateDataItem<ChatRoom>();
curRoom.Name = roomName;
curRoom.Users.Add(_currentUser); // the reverse reference will be automtically created

// save
var response = await App.Client.SaveAsync(false, TxnAutoInclusion.References, curRoom);

if(response.WasSuccessfull)
{
// log
ConsoleManager.WriteLine($"{DateTime.Now:T} | Room {roomName} created");
}
else
{
// reset dataitems
App.Client.ResetAllMonitoredItems();

// log
ConsoleManager.WriteLine($"{DateTime.Now:T} | Error: Could not create room {roomName}");
}
}
else if (!curRoom.Users.Contains(_currentUser)) // if room not contains user add the user to the room
{
// add
curRoom.Users.Add(_currentUser);

// save
var response = await App.Client.SaveAsync(false, TxnAutoInclusion.References, curRoom);

if (!response.WasSuccessfull)
{
// log
ConsoleManager.WriteLine($"{DateTime.Now:T} | Error: Could not join room {roomName}");

// return
return;
}
}

// cleanup previous event handler if existed
if (_currentRoom != null)
_currentRoom.Messages.DataItemListSaved -= Messages_DataItemListSaved;

// set current room
_currentRoom = curRoom;

// hook to message changes
_currentRoom.Messages.DataItemListSaved += Messages_DataItemListSaved;

// log
ConsoleManager.WriteLine($"{DateTime.Now:T} | - {_currentUser.Username} - joined room {roomName}");
}
else if (line.StartsWith("/leave ", StringComparison.OrdinalIgnoreCase)) // check if it is a leave command
{
// get the room name
var roomName = line.Replace("/leave", "", StringComparison.OrdinalIgnoreCase).Trim();

// get chatroom of user if exists
var curUserRoom = _currentUser.ChatRooms.ToList().FirstOrDefault(o => o.Name == roomName);

// check if found
if (curUserRoom != null)
{
// remove user
_currentUser.ChatRooms.Remove(curUserRoom);

// save
var response = await App.Client.SaveAsync(false, TxnAutoInclusion.References, _currentUser, curUserRoom);

// check
if (!response.WasSuccessfull)
{
// log
ConsoleManager.WriteLine($"{DateTime.Now:T} | Error: Could not leave room {roomName}");
}
else
{
// log
ConsoleManager.WriteLine($"{DateTime.Now:T} | Successfully left room {roomName}");
}
}
else
{
// log
ConsoleManager.WriteLine($"{DateTime.Now:T} | Error: Could not find room {roomName} to leave");
}
}
else // input is a standard message
{
// if current room is null then
if (_currentRoom == null)
{
// log
ConsoleManager.WriteLine($"{DateTime.Now:T} | Error: Please join a room using the command '/join <room name>' where <room name> is the name of the room, eg '/join athens_basketball'");
continue;
}
else
{
// create a message and save it
var newMsg = App.Client.CreateDataItem<ChatMessage>();
newMsg.Text = line;
newMsg.CreatedTime = DateTime.Now;
_currentRoom.Messages.Add(newMsg);
_currentUser.Messages.Add(newMsg); // set the author - reverse reference is automatically added
_lastMessageSent = newMsg; // cache it

// save
var response = await App.Client.SaveAsync(false, TxnAutoInclusion.References, newMsg, _currentUser, _currentRoom);

// check
if (!response.WasSuccessfull)
{
// log
ConsoleManager.WriteLine($"{DateTime.Now:T} | Error: Could not send message");
}
}
}
}
}