~ruther/NosTale-PacketLogger

aa49f2c91c461ef4866d076faaccf0148f10ab6b — František Boháček 2 years ago f312c7b
feat: add basic packet sender
23 files changed, 588 insertions(+), 106 deletions(-)

M src/PacketLogger/Models/Packets/CommsPacketProvider.cs
M src/PacketLogger/Models/Packets/DummyPacketProvider.cs
M src/PacketLogger/Models/Packets/FilePacketProvider.cs
M src/PacketLogger/Models/Packets/IPacketProvider.cs
D src/PacketLogger/Models/Packets/IPacketSender.cs
M src/PacketLogger/ViewModels/DockFactory.cs
R src/PacketLogger/ViewModels/{PacketLogDocumentViewModel.cs => DocumentViewModel.cs}
M src/PacketLogger/ViewModels/MainWindowViewModel.cs
R src/PacketLogger/ViewModels/{LogFilterTabViewModel.cs => PacketLogFilterViewModel.cs}
R src/PacketLogger/ViewModels/{LogTabViewModel.cs => PacketLogViewModel.cs}
A src/PacketLogger/ViewModels/PacketSendSubViewModel.cs
A src/PacketLogger/ViewModels/PacketSenderViewModel.cs
R src/PacketLogger/Views/{PacketLogDocumentView.axaml => DocumentView.axaml}
R src/PacketLogger/Views/{PacketLogDocumentView.axaml.cs => DocumentView.axaml.cs}
M src/PacketLogger/Views/MainWindow.axaml
R src/PacketLogger/Views/{LogFilterTabView.axaml => PacketLogFilterView.axaml}
R src/PacketLogger/Views/{LogFilterTabView.axaml.cs => PacketLogFilterView.axaml.cs}
R src/PacketLogger/Views/{LogTabView.axaml => PacketLogView.axaml}
R src/PacketLogger/Views/{LogTabView.axaml.cs => PacketLogView.axaml.cs}
A src/PacketLogger/Views/PacketSendSubView.axaml
A src/PacketLogger/Views/PacketSendSubView.axaml.cs
A src/PacketLogger/Views/PacketSenderView.axaml
A src/PacketLogger/Views/PacketSenderView.axaml.cs
M src/PacketLogger/Models/Packets/CommsPacketProvider.cs => src/PacketLogger/Models/Packets/CommsPacketProvider.cs +4 -1
@@ 20,7 20,7 @@ namespace PacketLogger.Models.Packets;
/// <summary>
/// A packet provider using a connection to a nostale client.
/// </summary>
public class CommsPacketProvider : IPacketSender
public class CommsPacketProvider : IPacketProvider
{
    private readonly Comms _comms;
    private long _currentIndex;


@@ 39,6 39,9 @@ public class CommsPacketProvider : IPacketSender
    public event PropertyChangedEventHandler? PropertyChanged;

    /// <inheritdoc />
    public string Name => "TODO";

    /// <inheritdoc />
    public bool IsOpen => _comms.Connection.Connection.State == ConnectionState.Open;

    /// <inheritdoc />

M src/PacketLogger/Models/Packets/DummyPacketProvider.cs => src/PacketLogger/Models/Packets/DummyPacketProvider.cs +34 -7
@@ 9,6 9,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using DynamicData;
using DynamicData.Binding;


@@ 22,25 23,34 @@ namespace PacketLogger.Models.Packets;
/// <inheritdoc />
public class DummyPacketProvider : IPacketProvider, IDisposable
{
    private long _index = 0;

    /// <summary>
    /// Initializes a new instance of the <see cref="DummyPacketProvider"/> class.
    /// </summary>
    public DummyPacketProvider()
    {
        var index = 0;
        Packets = new SourceList<PacketInfo>();
        Packets.Add(new PacketInfo(index++, DateTime.Now, PacketSource.Client, "#cl"));
        Packets.Add(new PacketInfo(index++, DateTime.Now, PacketSource.Client, "cl"));
        Packets.Add(new PacketInfo(_index++, DateTime.Now, PacketSource.Client, "#cl"));
        Packets.Add(new PacketInfo(_index++, DateTime.Now, PacketSource.Client, "cl"));
        for (var i = 0; i < 1000; i++)
        {
            Packets.Add
                (new PacketInfo(index++, DateTime.Now.AddSeconds(-1000 + i), PacketSource.Client, "walk 10 10"));
                (new PacketInfo(_index++, DateTime.Now.AddSeconds(-1000 + i), PacketSource.Client, "walk 10 10"));
            Packets.Add
                (new PacketInfo(index++, DateTime.Now.AddSeconds(-1000 + i), PacketSource.Server, "mv 1 50 52 123 123 89012390812 189023 182309 1823 189023 901283 091823 091823 901823 901283 091283 019283901283 901283 901 2831290 812390128390128213908139012839012839012390128390128938120938 1290 3190 adsadf"));
            (
                new PacketInfo
                (
                    _index++,
                    DateTime.Now.AddSeconds(-1000 + i),
                    PacketSource.Server,
                    "mv 1 50 52 123 123 89012390812 189023 182309 1823 189023 901283 091823 091823 901823 901283 091283 019283901283 901283 901 2831290 812390128390128213908139012839012839012390128390128938120938 1290 3190 adsadf"
                )
            );
            Packets.Add
                (new PacketInfo(index++, DateTime.Now.AddSeconds(-1000 + i), PacketSource.Client, "walk 12 14"));
                (new PacketInfo(_index++, DateTime.Now.AddSeconds(-1000 + i), PacketSource.Client, "walk 12 14"));
            Packets.Add
                (new PacketInfo(index++, DateTime.Now.AddSeconds(-1000 + i), PacketSource.Server, "mv 1 48 43"));
                (new PacketInfo(_index++, DateTime.Now.AddSeconds(-1000 + i), PacketSource.Server, "mv 1 48 43"));
        }
    }



@@ 62,6 72,9 @@ public class DummyPacketProvider : IPacketProvider, IDisposable
    }

    /// <inheritdoc />
    public string Name => "Empty";

    /// <inheritdoc />
    public bool IsOpen => false;

    /// <inheritdoc />


@@ 86,6 99,20 @@ public class DummyPacketProvider : IPacketProvider, IDisposable
    }

    /// <inheritdoc />
    public Task<Result> SendPacket(string packetString, CancellationToken ct = default)
    {
        Packets.Add(new PacketInfo(_index++, DateTime.Now, PacketSource.Client, packetString));
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public Task<Result> ReceivePacket(string packetString, CancellationToken ct = default)
    {
        Packets.Add(new PacketInfo(_index++, DateTime.Now, PacketSource.Server, packetString));
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public void Dispose()
    {
        Packets.Dispose();

M src/PacketLogger/Models/Packets/FilePacketProvider.cs => src/PacketLogger/Models/Packets/FilePacketProvider.cs +22 -3
@@ 11,6 11,7 @@ using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using DynamicData;
using NosSmooth.PacketSerializer.Abstractions.Attributes;


@@ 26,6 27,7 @@ public class FilePacketProvider : IPacketProvider
{
    private readonly string _fileName;
    private SourceList<PacketInfo>? _packets;
    private long _index = 0;

    /// <summary>
    /// Initializes a new instance of the <see cref="FilePacketProvider"/> class.


@@ 37,6 39,9 @@ public class FilePacketProvider : IPacketProvider
    }

    /// <inheritdoc />
    public string Name => Path.GetFileName(_fileName);

    /// <inheritdoc />
    public bool IsOpen => false;

    /// <inheritdoc />


@@ 66,7 71,7 @@ public class FilePacketProvider : IPacketProvider
        }

        var packets = new SourceList<PacketInfo>();
        var index = 0;
        _index = 0;
        foreach (var line in await File.ReadAllLinesAsync(_fileName))
        {
            if (line.Length <= 1)


@@ 81,7 86,7 @@ public class FilePacketProvider : IPacketProvider
                (
                    new PacketInfo
                    (
                        index++,
                        _index++,
                        DateTime.Now,
                        splitted[0] == "[Recv]" ? PacketSource.Server : PacketSource.Client,
                        splitted[1]


@@ 94,7 99,7 @@ public class FilePacketProvider : IPacketProvider
                (
                    new PacketInfo
                    (
                        index++,
                        _index++,
                        DateTime.Parse(splitted[0].Trim('[', ']')),
                        splitted[1] == "[Recv]" ? PacketSource.Server : PacketSource.Client,
                        splitted[2]


@@ 118,6 123,20 @@ public class FilePacketProvider : IPacketProvider
    }

    /// <inheritdoc />
    public Task<Result> SendPacket(string packetString, CancellationToken ct = default)
    {
        Packets.Add(new PacketInfo(_index++, DateTime.Now, PacketSource.Client, packetString));
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public Task<Result> ReceivePacket(string packetString, CancellationToken ct = default)
    {
        Packets.Add(new PacketInfo(_index++, DateTime.Now, PacketSource.Server, packetString));
        return Task.FromResult(Result.FromSuccess());
    }

    /// <inheritdoc />
    public event PropertyChangedEventHandler? PropertyChanged;

    /// <inheritdoc/>

M src/PacketLogger/Models/Packets/IPacketProvider.cs => src/PacketLogger/Models/Packets/IPacketProvider.cs +22 -0
@@ 8,6 8,7 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using DynamicData;
using PacketLogger.Models.Filters;


@@ 21,6 22,11 @@ namespace PacketLogger.Models.Packets;
public interface IPacketProvider : INotifyPropertyChanged, IDisposable
{
    /// <summary>
    /// Gets the name.
    /// </summary>
    public string Name { get; }

    /// <summary>
    /// Gets whether <see cref="Open"/> was called and successfully finished.
    /// </summary>
    public bool IsOpen { get; }


@@ 56,4 62,20 @@ public interface IPacketProvider : INotifyPropertyChanged, IDisposable
    /// Clear all packets.
    /// </summary>
    public void Clear();

    /// <summary>
    /// Send the given packets.
    /// </summary>
    /// <param name="packetString">The packet to send.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    Task<Result> SendPacket(string packetString, CancellationToken ct = default);

    /// <summary>
    /// Receive the given packet.
    /// </summary>
    /// <param name="packetString">The packet to send.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    Task<Result> ReceivePacket(string packetString, CancellationToken ct = default);
}
\ No newline at end of file

D src/PacketLogger/Models/Packets/IPacketSender.cs => src/PacketLogger/Models/Packets/IPacketSender.cs +0 -34
@@ 1,34 0,0 @@
//
//  IPacketSender.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Remora.Results;

namespace PacketLogger.Models.Packets;

/// <summary>
/// A provider that may as well send or receive packets.
/// </summary>
public interface IPacketSender : IPacketProvider
{
    /// <summary>
    /// Send the given packets.
    /// </summary>
    /// <param name="packetString">The packet to send.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    Task<Result> SendPacket(string packetString, CancellationToken ct = default);

    /// <summary>
    /// Receive the given packet.
    /// </summary>
    /// <param name="packetString">The packet to send.</param>
    /// <param name="ct">The cancellation token used for cancelling the operation.</param>
    /// <returns>A result that may or may not have succeeded.</returns>
    Task<Result> ReceivePacket(string packetString, CancellationToken ct = default);
}
\ No newline at end of file

M src/PacketLogger/ViewModels/DockFactory.cs => src/PacketLogger/ViewModels/DockFactory.cs +59 -8
@@ 6,7 6,9 @@

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive;
using System.Reactive.Linq;
using Dock.Avalonia.Controls;
using Dock.Model.Controls;
using Dock.Model.Core;


@@ 15,6 17,8 @@ using Dock.Model.Mvvm.Controls;
using NosSmooth.Comms.Local;
using NosSmooth.Core.Stateful;
using PacketLogger.Models;
using PacketLogger.Models.Packets;
using PacketLogger.Views;
using ReactiveUI;

namespace PacketLogger.ViewModels;


@@ 25,6 29,7 @@ namespace PacketLogger.ViewModels;
public class DockFactory : Factory, IDisposable
{
    private readonly StatefulRepository _repository;
    private readonly ObservableCollection<IPacketProvider> _providers;
    private readonly NostaleProcesses _processes;
    private readonly CommsInjector _injector;



@@ 34,20 39,42 @@ public class DockFactory : Factory, IDisposable
    /// <summary>
    /// Initializes a new instance of the <see cref="DockFactory"/> class.
    /// </summary>
    /// <param name="providers">The providers.</param>
    /// <param name="processes">The nostale processes.</param>
    /// <param name="injector">The communications injector.</param>
    /// <param name="repository">The repository.</param>
    public DockFactory(NostaleProcesses processes, CommsInjector injector, StatefulRepository repository)
    public DockFactory
    (
        ObservableCollection<IPacketProvider> providers,
        NostaleProcesses processes,
        CommsInjector injector,
        StatefulRepository repository
    )
    {
        _providers = providers;
        _processes = processes;
        _repository = repository;
        _injector = injector;
    }

    /// <inheritdoc />
    public override IDockWindow CreateDockWindow()
    /// <summary>
    /// Document loaded event.
    /// </summary>
    public event Action<DocumentViewModel>? DocumentLoaded;

    /// <summary>
    /// Document closed event.
    /// </summary>
    public event Action<DocumentViewModel>? DocumentClosed;

    private void OnDocumentLoaded(DocumentViewModel documentViewModel)
    {
        DocumentLoaded?.Invoke(documentViewModel);
    }

    private void OnDocumentClosed(DocumentViewModel documentViewModel)
    {
        return base.CreateDockWindow();
        DocumentClosed?.Invoke(documentViewModel);
    }

    /// <summary>


@@ 60,14 87,22 @@ public class DockFactory : Factory, IDisposable
    /// Creaate and load a document.
    /// </summary>
    /// <param name="load">The function to use to load the document.</param>
    public void CreateLoadedDocument(Func<PacketLogDocumentViewModel, IObservable<Unit>> load)
    public void CreateLoadedDocument(Func<DocumentViewModel, IObservable<Unit>> load)
    {
        if (_documentDock is null)
        {
            return;
        }

        var document = new PacketLogDocumentViewModel(_injector, _repository, _processes)
        var document = new DocumentViewModel
            (
                _injector,
                _repository,
                _providers,
                _processes,
                OnDocumentLoaded,
                OnDocumentClosed
            )
            { Id = $"New tab", Title = $"New tab" };

        var observable = load(document);


@@ 101,7 136,15 @@ public class DockFactory : Factory, IDisposable
                }

                var index = documentDock.VisibleDockables?.Count + 1;
                var document = new PacketLogDocumentViewModel(_injector, _repository, _processes)
                var document = new DocumentViewModel
                    (
                        _injector,
                        _repository,
                        _providers,
                        _processes,
                        OnDocumentLoaded,
                        OnDocumentClosed
                    )
                    { Id = $"New tab {index}", Title = $"New tab {index}" };

                AddDockable(documentDock, document);


@@ 115,7 158,15 @@ public class DockFactory : Factory, IDisposable
    /// <inheritdoc />
    public override IRootDock CreateLayout()
    {
        var initialTab = new PacketLogDocumentViewModel(_injector, _repository, _processes)
        var initialTab = new DocumentViewModel
            (
                _injector,
                _repository,
                _providers,
                _processes,
                OnDocumentLoaded,
                OnDocumentClosed
            )
            { Id = $"New tab", Title = $"New tab" };
        var documentDock = CreateDocumentDock();
        documentDock.IsCollapsable = false;

R src/PacketLogger/ViewModels/PacketLogDocumentViewModel.cs => src/PacketLogger/ViewModels/DocumentViewModel.cs +61 -10
@@ 1,5 1,5 @@
//
//  PacketLogDocumentViewModel.cs
//  DocumentViewModel.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.


@@ 31,23 31,39 @@ using ReactiveUI;
namespace PacketLogger.ViewModels;

/// <inheritdoc />
public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged, IDisposable
public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable
{
    private readonly CommsInjector _injector;
    private readonly ObservableCollection<IPacketProvider> _providers;
    private readonly NostaleProcesses _processes;
    private readonly Action<DocumentViewModel> _onDocumentUnloaded;
    private CancellationTokenSource _ctSource;
    private IPacketProvider? _packetProvider;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketLogDocumentViewModel"/> class.
    /// Initializes a new instance of the <see cref="DocumentViewModel"/> class.
    /// </summary>
    /// <param name="injector">The injector.</param>
    /// <param name="repository">The repository.</param>
    /// <param name="providers">The providers.</param>
    /// <param name="processes">The NosTale processes collection.</param>
    public PacketLogDocumentViewModel(CommsInjector injector, StatefulRepository repository, NostaleProcesses processes)
    /// <param name="onDocumentLoaded">The action to call on loaded.</param>
    /// <param name="onDocumentUnloaded">The action to call on document unloaded/closed.</param>
    public DocumentViewModel
    (
        CommsInjector injector,
        StatefulRepository repository,
        ObservableCollection<IPacketProvider> providers,
        NostaleProcesses processes,
        Action<DocumentViewModel> onDocumentLoaded,
        Action<DocumentViewModel> onDocumentUnloaded
    )
    {
        _ctSource = new CancellationTokenSource();
        _injector = injector;
        _providers = providers;
        _processes = processes;
        _onDocumentUnloaded = onDocumentUnloaded;
        OpenDummy = ReactiveCommand.CreateFromTask
        (
            () => Task.Run


@@ 56,8 72,10 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged, IDis
                {
                    Loading = true;
                    Name = "Dummy";
                    LogViewModel = new LogTabViewModel(new DummyPacketProvider());
                    _packetProvider = new DummyPacketProvider();
                    NestedViewModel = new PacketLogViewModel(_packetProvider);
                    Loaded = true;
                    onDocumentLoaded(this);
                }
            )
        );


@@ 82,6 100,7 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged, IDis

                var path = result[0];
                var provider = new FilePacketProvider(path);
                _packetProvider = provider;

                var openResult = await provider.Open();
                if (!openResult.IsSuccess)


@@ 91,9 110,10 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged, IDis
                }

                Title = Path.GetFileName(path);
                LogViewModel = new LogTabViewModel(provider);
                NestedViewModel = new PacketLogViewModel(provider);
                Loaded = true;
                Loading = false;
                onDocumentLoaded(this);
            }
        );



@@ 111,6 131,7 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged, IDis
                }

                var provider = new CommsPacketProvider(connection);
                _packetProvider = provider;
                repository.SetEntity<CommsPacketProvider>(connection.Client, provider);

                var contractResult = await connection.Connection.ContractHanshake


@@ 137,12 158,25 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged, IDis
                    )
                    .Subscribe();

                LogViewModel = new LogTabViewModel(provider);
                NestedViewModel = new PacketLogViewModel(provider);
                Title = handshakeResponse.CharacterName ?? $"Not in game ({process.Process.Id})";
                Loading = false;
                Loaded = true;
                onDocumentLoaded(this);
            }
        );

        OpenSender = ReactiveCommand.Create<IPacketProvider>
            (
                provider =>
                {
                    Loading = true;
                    NestedViewModel = new PacketSenderViewModel(provider);
                    Title = $"Sender ({provider.Name})";
                    Loaded = true;
                    Loading = false;
                }
            );
    }

    /// <summary>


@@ 151,6 185,16 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged, IDis
    public ObservableCollection<NostaleProcess> Processes => _processes.Processes;

    /// <summary>
    /// Gets packet provider.
    /// </summary>
    public IPacketProvider? Provider => _packetProvider;

    /// <summary>
    /// Gets the open providers.
    /// </summary>
    public ObservableCollection<IPacketProvider> Providers => _providers;

    /// <summary>
    /// Gets or sets the name of the tab.
    /// </summary>
    public string Name { get; set; } = "New tab";


@@ 168,7 212,12 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged, IDis
    /// <summary>
    /// Gets the log tab view model.
    /// </summary>
    public LogTabViewModel? LogViewModel { get; private set; }
    public ViewModelBase? NestedViewModel { get; private set; }

    /// <summary>
    /// Gets command for opening a dummy.
    /// </summary>
    public ReactiveCommand<IPacketProvider, Unit> OpenSender { get; }

    /// <summary>
    /// Gets command for opening a dummy.


@@ 188,7 237,8 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged, IDis
    /// <inheritdoc />
    public override bool OnClose()
    {
        LogViewModel?.Provider.Close().GetAwaiter().GetResult();
        _onDocumentUnloaded(this);
        _packetProvider?.Close().GetAwaiter().GetResult();
        Dispose();
        return base.OnClose();
    }


@@ 198,9 248,10 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged, IDis
    {
        _ctSource.Cancel();
        _ctSource.Dispose();
        LogViewModel?.Dispose();
        (NestedViewModel as IDisposable)?.Dispose();
        OpenDummy.Dispose();
        OpenFile.Dispose();
        OpenProcess.Dispose();
        OpenSender.Dispose();
    }
}
\ No newline at end of file

M src/PacketLogger/ViewModels/MainWindowViewModel.cs => src/PacketLogger/ViewModels/MainWindowViewModel.cs +48 -9
@@ 11,13 11,12 @@ using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Concurrency;
using System.Reflection;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Dock.Model.Controls;
using Dock.Model.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NosSmooth.Comms.Local.Extensions;


@@ 27,7 26,6 @@ using NosSmooth.PacketSerializer.Extensions;
using NosSmooth.PacketSerializer.Packets;
using PacketLogger.Models;
using PacketLogger.Models.Packets;
using PacketLogger.Views;
using ReactiveUI;

namespace PacketLogger.ViewModels;


@@ 47,6 45,7 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
            .AddLogging(b => b.ClearProviders().AddConsole())
            .AddSingleton<DockFactory>()
            .AddSingleton<NostaleProcesses>()
            .AddSingleton<ObservableCollection<IPacketProvider>>(_ => Providers)
            .AddNostaleCore()
            .AddStatefulInjector()
            .AddStatefulEntity<CommsPacketProvider>()


@@ 74,12 73,28 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
            }
        }

        _factory.DocumentLoaded += doc =>
        {
            if (doc.Provider is not null)
            {
                RxApp.MainThreadScheduler.Schedule(() => Providers.Add(doc.Provider));
            }
        };

        _factory.DocumentClosed += doc =>
        {
            if (doc.Provider is not null)
            {
                RxApp.MainThreadScheduler.Schedule(() => Providers.Remove(doc.Provider));
            }
        };

        SaveAll = ReactiveCommand.CreateFromTask
        (
            async () =>
            {
                if (Layout?.FocusedDockable is PacketLogDocumentViewModel activeDocument && activeDocument.Loaded
                    && activeDocument.LogViewModel is not null)
                if (Layout?.FocusedDockable is DocumentViewModel activeDocument && activeDocument.Loaded
                    && activeDocument.NestedViewModel is not null)
                {
                    var mainWindow = (App.Current!.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)
                        ?.MainWindow;


@@ 94,9 109,15 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
                        return;
                    }

                    if (activeDocument.Provider is null)
                    {
                        return;
                    }

                    using var file = File.OpenWrite(result);
                    using var streamWriter = new StreamWriter(file);
                    foreach (var packet in activeDocument.LogViewModel.Provider.Packets.Items)

                    foreach (var packet in activeDocument.Provider.Packets.Items)
                    {
                        await streamWriter.WriteLineAsync
                        (


@@ 111,8 132,8 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
        (
            async () =>
            {
                if (Layout?.FocusedDockable is PacketLogDocumentViewModel activeDocument && activeDocument.Loaded
                    && activeDocument.LogViewModel is not null)
                if (Layout?.FocusedDockable is DocumentViewModel activeDocument && activeDocument.Loaded
                    && activeDocument.NestedViewModel is not null)
                {
                    var mainWindow = (App.Current!.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)
                        ?.MainWindow;


@@ 126,9 147,14 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
                        return;
                    }

                    if (activeDocument.NestedViewModel is not PacketLogViewModel packetLogVM)
                    {
                        return;
                    }

                    using var file = File.OpenWrite(result);
                    using var streamWriter = new StreamWriter(file);
                    foreach (var packet in activeDocument.LogViewModel.FilteredPackets)
                    foreach (var packet in packetLogVM.FilteredPackets)
                    {
                        await streamWriter.WriteLineAsync
                        (


@@ 148,6 174,9 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
        Connect = ReactiveCommand.Create<IList>
            (process => _factory.CreateLoadedDocument(doc => doc.OpenProcess.Execute((NostaleProcess)process[0]!)));

        OpenSender = ReactiveCommand.Create<IList>
            (provider => _factory.CreateLoadedDocument(doc => doc.OpenSender.Execute((IPacketProvider)provider[0]!)));

        NewTab = ReactiveCommand.Create
            (() => _factory.DocumentDock.CreateDocument?.Execute(null));



@@ 161,6 190,11 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
    public ObservableCollection<NostaleProcess> Processes => _processes.Processes;

    /// <summary>
    /// Gets the packet provider.
    /// </summary>
    public ObservableCollection<IPacketProvider> Providers { get; } = new ObservableCollection<IPacketProvider>();

    /// <summary>
    /// Gets or sets the layout.
    /// </summary>
    public IRootDock? Layout { get; set; }


@@ 193,6 227,11 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
    /// <summary>
    /// Gets the command that opens empty logger.
    /// </summary>
    public ReactiveCommand<IList, Unit> OpenSender { get; }

    /// <summary>
    /// Gets the command that opens empty logger.
    /// </summary>
    public ReactiveCommand<IList, Unit> Connect { get; }

    /// <summary>

R src/PacketLogger/ViewModels/LogFilterTabViewModel.cs => src/PacketLogger/ViewModels/PacketLogFilterViewModel.cs +4 -4
@@ 1,5 1,5 @@
//
//  LogFilterTabViewModel.cs
//  PacketLogFilterViewModel.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.


@@ 17,12 17,12 @@ using ReactiveUI;
namespace PacketLogger.ViewModels;

/// <inheritdoc />
public class LogFilterTabViewModel : ViewModelBase, IDisposable
public class PacketLogFilterViewModel : ViewModelBase, IDisposable
{
    /// <summary>
    /// Initializes a new instance of the <see cref="LogFilterTabViewModel"/> class.
    /// Initializes a new instance of the <see cref="PacketLogFilterViewModel"/> class.
    /// </summary>
    public LogFilterTabViewModel()
    public PacketLogFilterViewModel()
    {
        Filters = new ObservableCollection<FilterCreator.FilterData>();
        RemoveCurrent = ReactiveCommand.Create

R src/PacketLogger/ViewModels/LogTabViewModel.cs => src/PacketLogger/ViewModels/PacketLogViewModel.cs +9 -9
@@ 1,5 1,5 @@
//
//  LogTabViewModel.cs
//  PacketLogViewModel.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.


@@ 26,7 26,7 @@ using Reloaded.Memory.Kernel32;
namespace PacketLogger.ViewModels;

/// <inheritdoc />
public class LogTabViewModel : ViewModelBase, IDisposable
public class PacketLogViewModel : ViewModelBase, IDisposable
{
    private readonly ReadOnlyObservableCollection<PacketInfo> _packets;
    private readonly IDisposable _cleanUp;


@@ 34,10 34,10 @@ public class LogTabViewModel : ViewModelBase, IDisposable
    private bool _logSent = true;

    /// <summary>
    /// Initializes a new instance of the <see cref="LogTabViewModel"/> class.
    /// Initializes a new instance of the <see cref="PacketLogViewModel"/> class.
    /// </summary>
    /// <param name="packetProvider">The packet provider.</param>
    public LogTabViewModel(IPacketProvider packetProvider)
    public PacketLogViewModel(IPacketProvider packetProvider)
    {
        Provider = packetProvider;



@@ 120,12 120,12 @@ public class LogTabViewModel : ViewModelBase, IDisposable
    /// <summary>
    /// Gets the send filter model.
    /// </summary>
    public LogFilterTabViewModel SendFilter { get; }
    public PacketLogFilterViewModel SendFilter { get; }

    /// <summary>
    /// Gets the receive filter model.
    /// </summary>
    public LogFilterTabViewModel RecvFilter { get; }
    public PacketLogFilterViewModel RecvFilter { get; }

    /// <summary>
    /// Gets the currently applied filter.


@@ 211,16 211,16 @@ public class LogTabViewModel : ViewModelBase, IDisposable
        CurrentFilter = new SendRecvFilter(sendFilter, recvFilter);
    }

    private IFilter CreateCompound(LogFilterTabViewModel logFilterTab)
    private IFilter CreateCompound(PacketLogFilterViewModel packetLogFilter)
    {
        List<IFilter> filters = new List<IFilter>();

        foreach (var filter in logFilterTab.Filters)
        foreach (var filter in packetLogFilter.Filters)
        {
            filters.Add(FilterCreator.BuildFilter(filter.Type, filter.Value));
        }

        return new CompoundFilter(!logFilterTab.Whitelist, filters.ToArray());
        return new CompoundFilter(!packetLogFilter.Whitelist, filters.ToArray());
    }

    /// <inheritdoc />

A src/PacketLogger/ViewModels/PacketSendSubViewModel.cs => src/PacketLogger/ViewModels/PacketSendSubViewModel.cs +130 -0
@@ 0,0 1,130 @@
//
//  PacketSendSubViewModel.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using PacketLogger.Models.Packets;
using ReactiveUI;

namespace PacketLogger.ViewModels;

/// <inheritdoc />
public class PacketSendSubViewModel : ViewModelBase, IDisposable
{
    private readonly IPacketProvider _sender;
    private string[]? _cachedPacketData;
    private IDisposable? _sendingTask;
    private SemaphoreSlim _semaphore;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketSendSubViewModel"/> class.
    /// </summary>
    /// <param name="source">The packet source to use.</param>
    /// <param name="sender">The sender to send packets to.</param>
    public PacketSendSubViewModel(PacketSource source, IPacketProvider sender)
    {
        _semaphore = new SemaphoreSlim(1, 1);
        Source = source;
        _sender = sender;

        SendPackets = ReactiveCommand.CreateFromTask(SendPacketData);
        ToggleRepetetiveSend = ReactiveCommand.Create(
            () =>
            {
                if (IsSending)
                {
                    _semaphore.Wait();
                    _sendingTask?.Dispose();
                    _sendingTask = null;
                    _semaphore.Release();
                }
                else
                {
                    _semaphore.Wait();
                    _cachedPacketData = null;
                    _sendingTask?.Dispose();
                    _sendingTask = Observable.Timer(DateTimeOffset.Now, TimeSpan.FromMilliseconds(RepetitionDelay))
                        .Subscribe
                        (
                            _ =>
                            {
                                SendPacketData().GetAwaiter().GetResult();
                            }
                        );
                    _semaphore.Release();
                }

                IsSending = !IsSending;
            });
    }

    /// <summary>
    /// Gets the source to send the packets as.
    /// </summary>
    public PacketSource Source { get; }

    /// <summary>
    /// Gets or sets whether current repetetively sending.
    /// </summary>
    public bool IsSending { get; private set; }

    /// <summary>
    /// Gets or sets the packets to send separated by a line.
    /// </summary>
    public string PacketsData { get; set; } = string.Empty;

    /// <summary>
    /// The delay of repetition in milliseconds.
    /// </summary>
    public int RepetitionDelay { get; set; } = 100;

    /// <summary>
    /// Gets or sets the command used to send the packets.
    /// </summary>
    public ReactiveCommand<Unit, Unit> SendPackets { get; }

    /// <summary>
    /// Gets the command used for toggling repetetive send.
    /// </summary>
    public ReactiveCommand<Unit, Unit> ToggleRepetetiveSend { get; }

    /// <inheritdoc />
    public void Dispose()
    {
        _sendingTask?.Dispose();
        SendPackets.Dispose();
        ToggleRepetetiveSend.Dispose();
    }

    private async Task SendPacketData()
    {
        if (!IsSending || _cachedPacketData is null)
        {
            _cachedPacketData = PacketsData.Split('\n', StringSplitOptions.RemoveEmptyEntries);
        }

        foreach (var line in _cachedPacketData)
        {
            await Send(line);
        }
    }

    private Task Send(string packetString)
    {
        if (Source == PacketSource.Server)
        {
            return _sender.ReceivePacket(packetString);
        }
        else
        {
            return _sender.SendPacket(packetString);
        }
    }
}
\ No newline at end of file

A src/PacketLogger/ViewModels/PacketSenderViewModel.cs => src/PacketLogger/ViewModels/PacketSenderViewModel.cs +44 -0
@@ 0,0 1,44 @@
//
//  PacketSenderViewModel.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Reactive;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
using PacketLogger.Models.Packets;
using ReactiveUI;

namespace PacketLogger.ViewModels;

/// <inheritdoc />
public class PacketSenderViewModel : ViewModelBase, IDisposable
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketSenderViewModel"/> class.
    /// </summary>
    /// <param name="packetSender">The packet sender.</param>
    public PacketSenderViewModel(IPacketProvider packetSender)
    {
        RecvSubViewModel = new PacketSendSubViewModel(PacketSource.Server, packetSender);
        SendSubViewModel = new PacketSendSubViewModel(PacketSource.Client, packetSender);
    }

    /// <summary>
    /// Gets the packet recv sub view.
    /// </summary>
    public PacketSendSubViewModel RecvSubViewModel { get; }

    /// <summary>
    /// Gets the packet send sub view.
    /// </summary>
    public PacketSendSubViewModel SendSubViewModel { get; }

    /// <inheritdoc />
    public void Dispose()
    {
        RecvSubViewModel.Dispose();
        SendSubViewModel.Dispose();
    }
}
\ No newline at end of file

R src/PacketLogger/Views/PacketLogDocumentView.axaml => src/PacketLogger/Views/DocumentView.axaml +21 -4
@@ 6,9 6,9 @@
             xmlns:viewModels="clr-namespace:PacketLogger.ViewModels"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
             x:Class="PacketLogger.Views.PacketLogDocumentView">
             x:Class="PacketLogger.Views.DocumentView">
    <Design.DataContext>
        <viewModels:PacketLogDocumentViewModel />
        <viewModels:DocumentViewModel />
    </Design.DataContext>

    <Grid>


@@ 82,11 82,28 @@

                    <TextBlock Grid.Row="0" Grid.Column="1" FontSize="34" Margin="-10,0,0,0" Text="Packet Sender" />

                    <TextBlock Grid.Row="1" Grid.Column="1" Text="To be implemented..." />
                    <StackPanel Grid.Row="1" Grid.Column="1" Grid.RowSpan="3" Orientation="Vertical">
                        <TextBlock FontSize="30" Margin="0,0,0,5" Text="Open a sender for" />
                        <DataGrid Margin="0,0,30,0" Items="{Binding Providers}">
                            <DataGrid.Columns>
                                <DataGridTextColumn Header="Tab" Binding="{Binding Name}" />
                                <DataGridTemplateColumn Header="Open">
                                    <DataGridTemplateColumn.CellTemplate>
                                        <DataTemplate>
                                            <Button Content="Open"
                                                    Command="{Binding $parent[UserControl].DataContext.OpenSender}"
                                                    IsEnabled="{Binding !$parent[UserControl].DataContext.Loading}"
                                                    CommandParameter="{Binding }" />
                                        </DataTemplate>
                                    </DataGridTemplateColumn.CellTemplate>
                                </DataGridTemplateColumn>
                            </DataGrid.Columns>
                        </DataGrid>
                    </StackPanel>
                </Grid>
            </Grid>
        </Border>

        <ContentControl IsVisible="{Binding Loaded}" Content="{Binding  LogViewModel}" />
        <ContentControl IsVisible="{Binding Loaded}" Content="{Binding  NestedViewModel}" />
    </Grid>
</UserControl>
\ No newline at end of file

R src/PacketLogger/Views/PacketLogDocumentView.axaml.cs => src/PacketLogger/Views/DocumentView.axaml.cs +4 -4
@@ 1,5 1,5 @@
//
//  PacketLogDocumentView.axaml.cs
//  DocumentView.axaml.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.


@@ 12,12 12,12 @@ using PropertyChanged;
namespace PacketLogger.Views;

[DoNotNotify]
public partial class PacketLogDocumentView : UserControl
public partial class DocumentView : UserControl
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketLogDocumentView"/> class.
    /// Initializes a new instance of the <see cref="DocumentView"/> class.
    /// </summary>
    public PacketLogDocumentView()
    public DocumentView()
    {
        InitializeComponent();
    }

M src/PacketLogger/Views/MainWindow.axaml => src/PacketLogger/Views/MainWindow.axaml +9 -1
@@ 40,7 40,15 @@
                <MenuItem Header="Exit" Command="{Binding QuitApplication}" />
            </MenuItem>
            <MenuItem Header="_Tools">
                <MenuItem Header="_Packet Sender" />
                <MenuItem Header="_Packet Sender" Command="{Binding Connect}" Items="{Binding Providers}">
                    <MenuItem.Styles>
                        <Style Selector="MenuItem">
                            <Setter Property="Header" Value="{Binding Name}" />
                            <Setter Property="Command" Value="{Binding OpenSender}" />
                            <Setter Property="CommandParameter" Value="{Binding SelectedItems, RelativeSource={RelativeSource Self}}" />
                        </Style>
                    </MenuItem.Styles>
                </MenuItem>
                <MenuItem Header="_Packet Analyzer" />
            </MenuItem>
        </Menu>

R src/PacketLogger/Views/LogFilterTabView.axaml => src/PacketLogger/Views/PacketLogFilterView.axaml +2 -2
@@ 4,9 4,9 @@
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="clr-namespace:PacketLogger.ViewModels"
             mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="450"
             x:Class="PacketLogger.Views.LogFilterTabView">
             x:Class="PacketLogger.Views.PacketLogFilterView">
    <Design.DataContext>
        <vm:LogFilterTabViewModel />
        <vm:PacketLogFilterViewModel />
    </Design.DataContext>

    <Grid ColumnDefinitions="*,*" RowDefinitions="*, 40, 40, 40">

R src/PacketLogger/Views/LogFilterTabView.axaml.cs => src/PacketLogger/Views/PacketLogFilterView.axaml.cs +4 -4
@@ 1,5 1,5 @@
//
//  LogFilterTabView.axaml.cs
//  PacketLogFilterView.axaml.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.


@@ 14,12 14,12 @@ using PropertyChanged;
namespace PacketLogger.Views;

[DoNotNotify]
public partial class LogFilterTabView : UserControl
public partial class PacketLogFilterView : UserControl
{
    /// <summary>
    /// Initializes a new instance of the <see cref="LogFilterTabView"/> class.
    /// Initializes a new instance of the <see cref="PacketLogFilterView"/> class.
    /// </summary>
    public LogFilterTabView()
    public PacketLogFilterView()
    {
        InitializeComponent();
        this.FindControl<ComboBox>("FilterType").Items = Enum.GetValues<FilterCreator.FilterType>();

R src/PacketLogger/Views/LogTabView.axaml => src/PacketLogger/Views/PacketLogView.axaml +2 -2
@@ 4,7 4,7 @@
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:vm="clr-namespace:PacketLogger.ViewModels"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="PacketLogger.Views.LogTabView"
             x:Class="PacketLogger.Views.PacketLogView"
             xmlns:i="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
             xmlns:converters="clr-namespace:PacketLogger.Converters"
             xmlns:views="clr-namespace:PacketLogger.Views"


@@ 15,7 15,7 @@
        <converters:PacketSourceConverter x:Key="packetSourceConverter" />
    </UserControl.Resources>
    <Design.DataContext>
        <vm:LogTabViewModel />
        <vm:PacketLogViewModel />
    </Design.DataContext>
    <SplitView IsPaneOpen="{Binding PaneOpen, Mode = TwoWay}" DisplayMode="CompactInline" PanePlacement="Right">
        <SplitView.Pane>

R src/PacketLogger/Views/LogTabView.axaml.cs => src/PacketLogger/Views/PacketLogView.axaml.cs +4 -4
@@ 1,5 1,5 @@
//
//  LogTabView.axaml.cs
//  PacketLogView.axaml.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.


@@ 13,12 13,12 @@ using PropertyChanged;
namespace PacketLogger.Views;

[DoNotNotify]
public partial class LogTabView : UserControl
public partial class PacketLogView : UserControl
{
    /// <summary>
    /// Initializes a new instance of the <see cref="LogTabView"/> class.
    /// Initializes a new instance of the <see cref="PacketLogView"/> class.
    /// </summary>
    public LogTabView()
    public PacketLogView()
    {
        InitializeComponent();
    }

A src/PacketLogger/Views/PacketSendSubView.axaml => src/PacketLogger/Views/PacketSendSubView.axaml +30 -0
@@ 0,0 1,30 @@
<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:viewModels="clr-namespace:PacketLogger.ViewModels"
             xmlns:converters="clr-namespace:PacketLogger.Converters"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="PacketLogger.Views.PacketSendSubView">
    <UserControl.Resources>
        <converters:PacketSourceConverter x:Key="packetSourceConverter" />
    </UserControl.Resources>
    <Design.DataContext>
        <viewModels:PacketSendSubViewModel />
    </Design.DataContext>

    <DockPanel>
        <StackPanel DockPanel.Dock="Right" Width="150">
            <Button Content="{Binding Source, Converter= {StaticResource packetSourceConverter}}"
                    Command="{Binding SendPackets}" />
            <TextBlock Text="Repeat delay" />
            <StackPanel>
                <NumericUpDown Value="{Binding RepetitionDelay}" />
                <TextBlock Text="ms" />
            </StackPanel>
            <Button Content="Start/Stop" Command="{Binding ToggleRepetetiveSend}" />
        </StackPanel>

        <TextBox Text="{Binding PacketsData}" AcceptsReturn="True" TextWrapping="NoWrap" Margin="0, 0, 20, 0" />
    </DockPanel>
</UserControl>
\ No newline at end of file

A src/PacketLogger/Views/PacketSendSubView.axaml.cs => src/PacketLogger/Views/PacketSendSubView.axaml.cs +29 -0
@@ 0,0 1,29 @@
//
//  PacketSendSubView.axaml.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PropertyChanged;

namespace PacketLogger.Views;

[DoNotNotify]
public partial class PacketSendSubView : UserControl
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketSendSubView"/> class.
    /// </summary>
    public PacketSendSubView()
    {
        InitializeComponent();
    }

    private void InitializeComponent()
    {
        AvaloniaXamlLoader.Load(this);
    }
}
\ No newline at end of file

A src/PacketLogger/Views/PacketSenderView.axaml => src/PacketLogger/Views/PacketSenderView.axaml +17 -0
@@ 0,0 1,17 @@
<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:viewModels="clr-namespace:PacketLogger.ViewModels"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="PacketLogger.Views.PacketSenderView">
    <Design.DataContext>
        <viewModels:PacketSenderViewModel />
    </Design.DataContext>
    <StackPanel Orientation="Vertical" Margin="10">
        <TextBlock Text="Recv" Margin="0,0,0,5" />
        <ContentControl Content="{Binding RecvSubViewModel}" />
        <TextBlock Text="Send" Margin="0,5,0,5" />
        <ContentControl Content="{Binding SendSubViewModel}" />
    </StackPanel>
</UserControl>
\ No newline at end of file

A src/PacketLogger/Views/PacketSenderView.axaml.cs => src/PacketLogger/Views/PacketSenderView.axaml.cs +29 -0
@@ 0,0 1,29 @@
//
//  PacketSenderView.axaml.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using PropertyChanged;

namespace PacketLogger.Views;

[DoNotNotify]
public partial class PacketSenderView : UserControl
{
    /// <summary>
    /// Initializes a new instance of the <see cref="PacketSenderView"/> class.
    /// </summary>
    public PacketSenderView()
    {
        InitializeComponent();
    }

    private void InitializeComponent()
    {
        AvaloniaXamlLoader.Load(this);
    }
}
\ No newline at end of file

Do not follow this link