A src/PacketLogger/Models/PacketComparer.cs => src/PacketLogger/Models/PacketComparer.cs +20 -0
@@ 0,0 1,20 @@
+//
+// PacketComparer.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.Collections.Generic;
+using PacketLogger.Models.Packets;
+
+namespace PacketLogger.Models;
+
+/// <inheritdoc />
+public class PacketComparer : IComparer<PacketInfo>
+{
+ /// <inheritdoc />
+ public int Compare(PacketInfo x, PacketInfo y)
+ {
+ return x.PacketIndex.CompareTo(y.PacketIndex);
+ }
+}<
\ No newline at end of file
A src/PacketLogger/Models/Packets/CommsPacketProvider.cs => src/PacketLogger/Models/Packets/CommsPacketProvider.cs +92 -0
@@ 0,0 1,92 @@
+//
+// CommsPacketProvider.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.ComponentModel;
+using System.Data;
+using System.Threading;
+using System.Threading.Tasks;
+using DynamicData;
+using NosSmooth.Comms.Local;
+using NosSmooth.Core.Packets;
+using NosSmooth.PacketSerializer.Abstractions.Attributes;
+using Remora.Results;
+
+namespace PacketLogger.Models.Packets;
+
+/// <summary>
+/// A packet provider using a connection to a nostale client.
+/// </summary>
+public class CommsPacketProvider : IPacketSender
+{
+ private readonly Comms _comms;
+ private long _currentIndex;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CommsPacketProvider"/> class.
+ /// </summary>
+ /// <param name="comms">The comms.</param>
+ public CommsPacketProvider(Comms comms)
+ {
+ _comms = comms;
+ Packets = new SourceList<PacketInfo>();
+ }
+
+ /// <inheritdoc />
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ /// <inheritdoc />
+ public bool IsOpen => _comms.Connection.Connection.State == ConnectionState.Open;
+
+ /// <inheritdoc />
+ public SourceList<PacketInfo> Packets { get; }
+
+ /// <inheritdoc />
+ public bool LogReceived { get; set; } = true;
+
+ /// <inheritdoc />
+ public bool LogSent { get; set; } = true;
+
+ /// <inheritdoc />
+ public Task<Result> Open()
+ => Task.FromResult(Result.FromSuccess());
+
+ /// <inheritdoc />
+ public Task<Result> Close()
+ {
+ _comms.Connection.Connection.Disconnect();
+ return Task.FromResult(Result.FromSuccess());
+ }
+
+ /// <inheritdoc />
+ public void Clear()
+ {
+ Packets.Clear();
+ }
+
+ /// <inheritdoc />
+ public Task<Result> SendPacket(string packetString, CancellationToken ct = default)
+ => _comms.Client.SendPacketAsync(packetString, ct);
+
+ /// <inheritdoc />
+ public Task<Result> ReceivePacket(string packetString, CancellationToken ct = default)
+ => _comms.Client.ReceivePacketAsync(packetString, ct);
+
+ /// <summary>
+ /// Add the given packets from an event.
+ /// </summary>
+ /// <param name="packetArgs">The packet event args.</param>
+ /// <typeparam name="TPacket">The type of the deserialized packet.</typeparam>
+ internal void AddPacket<TPacket>(PacketEventArgs<TPacket> packetArgs)
+ {
+ var index = Interlocked.Increment(ref _currentIndex);
+ if ((packetArgs.Source == PacketSource.Server && LogReceived)
+ || (packetArgs.Source == PacketSource.Client && LogSent))
+ {
+ Packets.Add(new PacketInfo(index, DateTime.Now, packetArgs.Source, packetArgs.PacketString));
+ }
+ }
+}<
\ No newline at end of file
M src/PacketLogger/Models/Packets/DummyPacketProvider.cs => src/PacketLogger/Models/Packets/DummyPacketProvider.cs +14 -0
@@ 48,6 48,20 @@ public class DummyPacketProvider : IPacketProvider, IDisposable
public event PropertyChangedEventHandler? PropertyChanged;
/// <inheritdoc />
+ public bool LogReceived
+ {
+ get => true;
+ set { }
+ }
+
+ /// <inheritdoc />
+ public bool LogSent
+ {
+ get => true;
+ set { }
+ }
+
+ /// <inheritdoc />
public bool IsOpen => false;
/// <inheritdoc />
M src/PacketLogger/Models/Packets/FilePacketProvider.cs => src/PacketLogger/Models/Packets/FilePacketProvider.cs +14 -0
@@ 40,6 40,20 @@ public class FilePacketProvider : IPacketProvider
public bool IsOpen => false;
/// <inheritdoc />
+ public bool LogReceived
+ {
+ get => true;
+ set { }
+ }
+
+ /// <inheritdoc />
+ public bool LogSent
+ {
+ get => true;
+ set { }
+ }
+
+ /// <inheritdoc />
public SourceList<PacketInfo> Packets
=> _packets ?? throw new InvalidOperationException("File client not initialized yet.");
M src/PacketLogger/Models/Packets/IPacketProvider.cs => src/PacketLogger/Models/Packets/IPacketProvider.cs +10 -0
@@ 25,6 25,16 @@ public interface IPacketProvider : INotifyPropertyChanged
public bool IsOpen { get; }
/// <summary>
+ /// Gets or sets whether to log received packets.
+ /// </summary>
+ public bool LogReceived { get; set; }
+
+ /// <summary>
+ /// Gets or sets whether to log sent pckets.
+ /// </summary>
+ public bool LogSent { get; set; }
+
+ /// <summary>
/// Gets the filtered packets from this provider.
/// </summary>
public SourceList<PacketInfo> Packets { get; }
M src/PacketLogger/Models/Packets/PacketInfo.cs => src/PacketLogger/Models/Packets/PacketInfo.cs +3 -1
@@ 5,8 5,10 @@
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
+using System.Diagnostics.CodeAnalysis;
using NosSmooth.PacketSerializer.Abstractions.Attributes;
namespace PacketLogger.Models.Packets;
-public record PacketInfo(long PacketIndex, DateTime Date, PacketSource Source, string PacketString);>
\ No newline at end of file
+[SuppressMessage("StyleCop.CSharp.NamingRules", "SA1313:Parameter names should begin with lower-case letter", Justification = "Fix this.")]
+public record struct PacketInfo(long PacketIndex, DateTime Date, PacketSource Source, string PacketString);<
\ No newline at end of file
A src/PacketLogger/Models/Packets/PacketResponder.cs => src/PacketLogger/Models/Packets/PacketResponder.cs +40 -0
@@ 0,0 1,40 @@
+//
+// PacketResponder.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 DynamicData;
+using NosSmooth.Comms.Core;
+using NosSmooth.Comms.Inject;
+using NosSmooth.Comms.Inject.PacketResponders;
+using NosSmooth.Core.Packets;
+using NosSmooth.Packets;
+using Remora.Results;
+
+namespace PacketLogger.Models.Packets;
+
+/// <inheritdoc />
+public class PacketResponder : IEveryPacketResponder
+{
+ private readonly CommsPacketProvider _provider;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="PacketResponder"/> class.
+ /// </summary>
+ /// <param name="provider">The provider.</param>
+ public PacketResponder(CommsPacketProvider provider)
+ {
+ _provider = provider;
+ }
+
+ /// <inheritdoc />
+ public Task<Result> Respond<TPacket>(PacketEventArgs<TPacket> packetArgs, CancellationToken ct = default)
+ where TPacket : IPacket
+ {
+ _provider.AddPacket(packetArgs);
+ return Task.FromResult(Result.FromSuccess());
+ }
+}<
\ No newline at end of file
M src/PacketLogger/PacketLogger.csproj => src/PacketLogger/PacketLogger.csproj +9 -3
@@ 1,6 1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
- <OutputType>WinExe</OutputType>
+ <OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
@@ 27,12 27,18 @@
<PackageReference Include="DataBox" Version="0.10.13" />
<PackageReference Include="Dock.Avalonia" Version="0.10.18" />
<PackageReference Include="Dock.Model.Mvvm" Version="0.10.18" />
- <PackageReference Include="NosSmooth.Comms.Local" Version="1.0.2" />
+ <PackageReference Include="NosSmooth.Comms.Local" Version="1.0.3">
+ <IncludeAssets>All</IncludeAssets>
+ <PrivateAssets>None</PrivateAssets>
+ </PackageReference>
+ <PackageReference Include="NosSmooth.Core" Version="3.4.2-main4077986910" />
<PackageReference Include="NosSmooth.LocalBinding" Version="1.0.0" />
<PackageReference Include="NosSmooth.PacketSerializer.Abstractions" Version="1.3.1" />
<PackageReference Include="Projektanker.Icons.Avalonia" Version="5.8.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="5.8.0" />
- <PackageReference Include="PropertyChanged.Fody" Version="4.1.0" />
+ <PackageReference Include="PropertyChanged.Fody" Version="4.1.0">
+ <PrivateAssets>All</PrivateAssets>
+ </PackageReference>
<PackageReference Include="Remora.Results" Version="7.2.3" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.5.1" />
</ItemGroup>
M src/PacketLogger/ViewModels/DockFactory.cs => src/PacketLogger/ViewModels/DockFactory.cs +12 -7
@@ 11,6 11,8 @@ using Dock.Model.Controls;
using Dock.Model.Core;
using Dock.Model.Mvvm;
using Dock.Model.Mvvm.Controls;
+using NosSmooth.Comms.Local;
+using NosSmooth.Core.Stateful;
using PacketLogger.Models;
using ReactiveUI;
@@ 21,7 23,9 @@ namespace PacketLogger.ViewModels;
/// </summary>
public class DockFactory : Factory, IDisposable
{
- private NostaleProcesses _processes = new();
+ private readonly StatefulRepository _repository;
+ private readonly NostaleProcesses _processes = new();
+ private readonly CommsInjector _injector;
/// <inheritdoc />
public override IDocumentDock CreateDocumentDock()
@@ 37,7 41,7 @@ public class DockFactory : Factory, IDisposable
}
var index = documentDock.VisibleDockables?.Count + 1;
- var document = new PacketLogDocumentViewModel(_processes)
+ var document = new PacketLogDocumentViewModel(_injector, _repository, _processes)
{ Id = $"New tab {index}", Title = $"New tab {index}" };
AddDockable(documentDock, document);
@@ 48,23 52,24 @@ public class DockFactory : Factory, IDisposable
return documentDock;
}
- private readonly object _context;
private IRootDock? _rootDock;
private IDocumentDock? _documentDock;
/// <summary>
/// Initializes a new instance of the <see cref="DockFactory"/> class.
/// </summary>
- /// <param name="context">The context.</param>
- public DockFactory(object context)
+ /// <param name="injector">The communications injector.</param>
+ /// <param name="repository">The repository.</param>
+ public DockFactory(CommsInjector injector, StatefulRepository repository)
{
- _context = context;
+ _repository = repository;
+ _injector = injector;
}
/// <inheritdoc />
public override IRootDock CreateLayout()
{
- var initialTab = new PacketLogDocumentViewModel(_processes)
+ var initialTab = new PacketLogDocumentViewModel(_injector, _repository, _processes)
{ Id = $"New tab", Title = $"New tab" };
var documentDock = CreateDocumentDock();
documentDock.IsCollapsable = false;
M src/PacketLogger/ViewModels/LogTabViewModel.cs => src/PacketLogger/ViewModels/LogTabViewModel.cs +50 -6
@@ 10,13 10,18 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
+using System.Reactive.Concurrency;
+using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia;
+using Avalonia.Collections;
using DynamicData;
using DynamicData.Binding;
+using PacketLogger.Models;
using PacketLogger.Models.Filters;
using PacketLogger.Models.Packets;
using ReactiveUI;
+using Reloaded.Memory.Kernel32;
namespace PacketLogger.ViewModels;
@@ 24,7 29,9 @@ namespace PacketLogger.ViewModels;
public class LogTabViewModel : ViewModelBase, IDisposable
{
private readonly ReadOnlyObservableCollection<PacketInfo> _packets;
- private IDisposable _packetsSubscription;
+ private readonly IDisposable _cleanUp;
+ private bool _logReceived = true;
+ private bool _logSent = true;
/// <summary>
/// Initializes a new instance of the <see cref="LogTabViewModel"/> class.
@@ 51,14 58,30 @@ public class LogTabViewModel : ViewModelBase, IDisposable
}
);
- _packetsSubscription = Provider.Packets.Connect()
+ var packetsSubscription = Provider.Packets.Connect()
.Filter(dynamicFilter)
- .Sort(SortExpressionComparer<PacketInfo>.Ascending(x => x.PacketIndex))
+ .Sort(new PacketComparer())
.Bind(out _packets)
.ObserveOn(RxApp.MainThreadScheduler)
.DisposeMany()
.Subscribe();
+ var scrollSubscription = FilteredPackets.ObserveCollectionChanges()
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .Do
+ (
+ change =>
+ {
+ if (Scroll && change.EventArgs.NewItems is not null)
+ {
+ var last = FilteredPackets[^1];
+ RxApp.MainThreadScheduler.Schedule(DateTimeOffset.Now.AddMilliseconds(1), () => SelectedPacket = last);
+ }
+ }
+ )
+ .Subscribe();
+
+ _cleanUp = new CompositeDisposable(scrollSubscription, packetsSubscription);
CopyPackets = ReactiveCommand.CreateFromObservable<IList, Unit>
(
list => Observable.StartAsync
@@ 151,12 174,28 @@ public class LogTabViewModel : ViewModelBase, IDisposable
/// <summary>
/// Gets or sets whether to log received packets.
/// </summary>
- public bool LogReceived { get; set; } = true;
+ public bool LogReceived
+ {
+ get => _logReceived;
+ set
+ {
+ Provider.LogReceived = value;
+ _logReceived = value;
+ }
+ }
/// <summary>
/// Gets or sets whether to log sent packets.
/// </summary>
- public bool LogSent { get; set; } = true;
+ public bool LogSent
+ {
+ get => _logSent;
+ set
+ {
+ Provider.LogSent = value;
+ _logSent = value;
+ }
+ }
/// <summary>
/// Gets or sets whether to scroll to teh bottom of the grid.
@@ 164,6 203,11 @@ public class LogTabViewModel : ViewModelBase, IDisposable
public bool Scroll { get; set; } = true;
/// <summary>
+ /// Gets or sets the currently selected packet.
+ /// </summary>
+ public object? SelectedPacket { get; set; }
+
+ /// <summary>
/// Gets empty string.
/// </summary>
public string Empty { get; } = string.Empty;
@@ 192,6 236,6 @@ public class LogTabViewModel : ViewModelBase, IDisposable
public void Dispose()
{
TogglePane.Dispose();
- _packetsSubscription.Dispose();
+ _cleanUp.Dispose();
}
}=
\ No newline at end of file
M src/PacketLogger/ViewModels/MainWindowViewModel.cs => src/PacketLogger/ViewModels/MainWindowViewModel.cs +25 -1
@@ 4,10 4,17 @@
// 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.Collections.ObjectModel;
using System.ComponentModel;
using Dock.Model.Controls;
using Dock.Model.Core;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using NosSmooth.Comms.Local.Extensions;
+using NosSmooth.Core.Extensions;
+using NosSmooth.PacketSerializer.Extensions;
+using NosSmooth.PacketSerializer.Packets;
using PacketLogger.Models;
using PacketLogger.Models.Packets;
@@ 23,7 30,24 @@ public class MainWindowViewModel : ViewModelBase, INotifyPropertyChanged
/// </summary>
public MainWindowViewModel()
{
- _factory = new DockFactory(this);
+ var services = new ServiceCollection()
+ .AddLogging(b => b.ClearProviders().AddConsole())
+ .AddSingleton<DockFactory>()
+ .AddNostaleCore()
+ .AddStatefulInjector()
+ .AddStatefulEntity<CommsPacketProvider>()
+ .AddLocalComms()
+ .AddPacketResponder<PacketResponder>()
+ .BuildServiceProvider();
+
+ var packetTypes = services.GetRequiredService<IPacketTypesRepository>();
+ var result = packetTypes.AddDefaultPackets();
+ if (!result.IsSuccess)
+ {
+ Console.WriteLine(result.ToFullString());
+ }
+
+ _factory = services.GetRequiredService<DockFactory>();
Layout = _factory?.CreateLayout();
if (Layout is { })
M src/PacketLogger/ViewModels/PacketLogDocumentViewModel.cs => src/PacketLogger/ViewModels/PacketLogDocumentViewModel.cs +78 -9
@@ 13,10 13,17 @@ using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
+using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Dock.Model.Mvvm.Controls;
+using DynamicData.Binding;
+using NosSmooth.Comms.Data.Messages;
+using NosSmooth.Comms.Local;
+using NosSmooth.Core.Contracts;
+using NosSmooth.Core.Extensions;
+using NosSmooth.Core.Stateful;
using PacketLogger.Models;
using PacketLogger.Models.Packets;
using ReactiveUI;
@@ 26,25 33,35 @@ namespace PacketLogger.ViewModels;
/// <inheritdoc />
public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged
{
+ private readonly CommsInjector _injector;
private readonly NostaleProcesses _processes;
+ private CancellationTokenSource _ctSource;
/// <summary>
/// Initializes a new instance of the <see cref="PacketLogDocumentViewModel"/> class.
/// </summary>
+ /// <param name="injector">The injector.</param>
+ /// <param name="repository">The repository.</param>
/// <param name="processes">The NosTale processes collection.</param>
- public PacketLogDocumentViewModel(NostaleProcesses processes)
+ public PacketLogDocumentViewModel(CommsInjector injector, StatefulRepository repository, NostaleProcesses processes)
{
+ _ctSource = new CancellationTokenSource();
+ _injector = injector;
_processes = processes;
OpenDummy = ReactiveCommand.CreateFromTask
(
- () => Task.Run(() =>
- {
- Loading = true;
- Name = "Dummy";
- LogViewModel = new LogTabViewModel(new DummyPacketProvider());
- Loaded = true;
- })
+ () => Task.Run
+ (
+ () =>
+ {
+ Loading = true;
+ Name = "Dummy";
+ LogViewModel = new LogTabViewModel(new DummyPacketProvider());
+ Loaded = true;
+ }
+ )
);
+
OpenFile = ReactiveCommand.CreateFromTask
(
async () =>
@@ 79,6 96,53 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged
Loading = false;
}
);
+
+ OpenProcess = ReactiveCommand.CreateFromTask<NostaleProcess>
+ (
+ async (process, ct) =>
+ {
+ Loading = true;
+ var connectionResult = await injector.EstablishNamedPipesConnectionAsync
+ (process.Process, _ctSource.Token, ct);
+ if (!connectionResult.IsDefined(out var connection))
+ {
+ Console.WriteLine(connectionResult.ToFullString());
+ return;
+ }
+
+ var provider = new CommsPacketProvider(connection);
+ repository.SetEntity<CommsPacketProvider>(connection.Client, provider);
+
+ var contractResult = await connection.Connection.ContractHanshake
+ (new HandshakeRequest("PacketLogger", true, false))
+ .WaitForAsync(DefaultStates.ResponseObtained, ct: ct);
+
+ if (!contractResult.IsDefined(out var handshakeResponse))
+ {
+ repository.Remove(connection.Client);
+ Console.WriteLine(contractResult.ToFullString());
+ return;
+ }
+
+ process.WhenPropertyChanged(x => x.CharacterString)
+ .ObserveOn(RxApp.MainThreadScheduler)
+ .Do
+ (
+ _ =>
+ {
+ Title = (process.BrowserManager.IsInGame
+ ? process.BrowserManager.PlayerManager.Player.Name
+ : null) ?? $"Not in game ({process.Process.Id})";
+ }
+ )
+ .Subscribe();
+
+ LogViewModel = new LogTabViewModel(provider);
+ Title = handshakeResponse.CharacterName ?? $"Not in game ({process.Process.Id})";
+ Loading = false;
+ Loaded = true;
+ }
+ );
}
/// <summary>
@@ 94,7 158,7 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged
/// <summary>
/// Gets whether the document is currently being loaded.
/// </summary>
- public bool Loading { get; private set; } = false;
+ public bool Loading { get; private set; }
/// <summary>
/// Gets whether a document has been loaded.
@@ 115,4 179,9 @@ public class PacketLogDocumentViewModel : Document, INotifyPropertyChanged
/// Gets command for opening a file.
/// </summary>
public ReactiveCommand<Unit, Unit> OpenFile { get; }
+
+ /// <summary>
+ /// Gets the command for opening a process / connecting to a process.
+ /// </summary>
+ public ReactiveCommand<NostaleProcess, Unit> OpenProcess { get; }
}=
\ No newline at end of file
M src/PacketLogger/Views/LogTabView.axaml => src/PacketLogger/Views/LogTabView.axaml +2 -0
@@ 65,6 65,8 @@
<DataGrid Items="{Binding FilteredPackets}" IsReadOnly="True" CanUserSortColumns="False"
x:Name="PacketsLog"
+ SelectedItem="{Binding SelectedPacket, Mode=TwoWay}"
+ SelectionChanged="PacketsLog_OnSelectionChanged"
CanUserReorderColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Time" Binding="{Binding Date, StringFormat = {}{0:HH:mm:ss}}"
M src/PacketLogger/Views/LogTabView.axaml.cs => src/PacketLogger/Views/LogTabView.axaml.cs +9 -0
@@ 4,6 4,7 @@
// 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.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
@@ 26,4 27,12 @@ public partial class LogTabView : UserControl
{
AvaloniaXamlLoader.Load(this);
}
+
+ private void PacketsLog_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ if (sender is DataGrid dataGrid && dataGrid.SelectedItem is not null)
+ {
+ dataGrid.ScrollIntoView(dataGrid.SelectedItem, dataGrid.Columns.First());
+ }
+ }
}=
\ No newline at end of file
M src/PacketLogger/Views/PacketLogDocumentView.axaml => src/PacketLogger/Views/PacketLogDocumentView.axaml +6 -3
@@ 13,7 13,7 @@
<Grid>
<Border IsVisible="{Binding !Loaded}"
- MaxWidth="800" MaxHeight="600"
+ MaxWidth="1000" MaxHeight="600"
CornerRadius="25"
Background="{DynamicResource SystemControlPageBackgroundChromeLowBrush}">
<Grid Margin="50">
@@ 69,7 69,10 @@
<DataGridTemplateColumn Header="Connect">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
- <Button Content="Connect" />
+ <Button Content="Connect"
+ Command="{Binding $parent[UserControl].DataContext.OpenProcess}"
+ IsEnabled="{Binding !$parent[UserControl].DataContext.Loading}"
+ CommandParameter="{Binding }" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
@@ 78,7 81,7 @@
</StackPanel>
<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..." />
</Grid>
</Grid>