1
0
mirror of synced 2024-11-23 21:13:50 +00:00

Started major refactoring

- Upgrade to .NET 5
- Split functionality into libraries
- Tabbed interface for multiple subscribers
- Placeholder for publishing tab
This commit is contained in:
Mark van Renswoude 2021-11-28 14:18:21 +01:00
parent 12d2161117
commit 2755e93440
92 changed files with 3520 additions and 2556 deletions

11
.editorconfig Normal file
View File

@ -0,0 +1,11 @@
[*.cs]
# IDE0011: Add braces
csharp_prefer_braces = when_multiline
csharp_style_var_for_built_in_types=true:silent
csharp_style_var_when_type_is_apparent=true:silent
csharp_style_var_elsewhere=true:silent
dotnet_diagnostic.IDE0055.severity = none
dotnet_diagnostic.IDE0130.severity = none

190
.gitignore vendored
View File

@ -1,189 +1,5 @@
## Ignore Visual Studio temporary files, build results, and .VS
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user *.user
*.sln.docstates
# Build results bin
[Dd]ebug/ obj
[Dd]ebugPublic/
[Rr]elease/
x64/
build/
bld/
[Bb]in/
[Oo]bj/
# Roslyn cache directories
*.ide/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
#NUNIT
*.VisualState.xml
TestResult.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# JustCode is a .NET coding addin-in
.JustCode
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# NCrunch
_NCrunch_*
.*crunch*.local.xml
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
## TODO: Comment the next line if you want to checkin your
## web deploy settings but do note that will include unencrypted
## passwords
#*.pubxml
# NuGet Packages Directory
packages/*
## TODO: If the tool you use requires repositories.config
## uncomment the next line
#!packages/repositories.config
# Enable "build/" folder in the NuGet Packages folder since
# NuGet packages use it for MSBuild targets.
# This line needs to be after the ignore of the build folder
# (and the packages folder if the line above has been uncommented)
!packages/build/
# Windows Azure Build Output
csx/
*.build.csdef
# Windows Store app package directory
AppPackages/
# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.pfx
*.publishsettings
node_modules/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
# SQL Server files
*.mdf
*.ldf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
# Microsoft Fakes
FakesAssemblies/
# LightSwitch generated files
GeneratedArtifacts/
_Pvt_Extensions/
ModelManifest.xml

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>

View File

@ -1,483 +0,0 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DrawingImage x:Key="ClearIcon">
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup>
<DrawingGroup.ClipGeometry>
<RectangleGeometry Rect="0,0,58,58" />
</DrawingGroup.ClipGeometry>
<DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FFEFEBDE">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M46.5,14L46.5,14 32.5,0 1.5,0 1.5,58 46.5,58z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<DrawingGroup>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M11.5,23L36.5,23C37.052,23 37.5,22.553 37.5,22 37.5,21.447 37.052,21 36.5,21L11.5,21C10.948,21 10.5,21.447 10.5,22 10.5,22.553 10.948,23 11.5,23z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M11.5,15L21.5,15C22.052,15 22.5,14.553 22.5,14 22.5,13.447 22.052,13 21.5,13L11.5,13C10.948,13 10.5,13.447 10.5,14 10.5,14.553 10.948,15 11.5,15z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M36.5,29L11.5,29C10.948,29 10.5,29.447 10.5,30 10.5,30.553 10.948,31 11.5,31L36.5,31C37.052,31 37.5,30.553 37.5,30 37.5,29.447 37.052,29 36.5,29z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M36.5,37L11.5,37C10.948,37 10.5,37.447 10.5,38 10.5,38.553 10.948,39 11.5,39L36.5,39C37.052,39 37.5,38.553 37.5,38 37.5,37.447 37.052,37 36.5,37z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M36.5,45L11.5,45C10.948,45 10.5,45.447 10.5,46 10.5,46.553 10.948,47 11.5,47L36.5,47C37.052,47 37.5,46.553 37.5,46 37.5,45.447 37.052,45 36.5,45z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M32.5,0L32.5,0 32.5,14 46.5,14z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FFED7161">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="12" RadiusY="12" Center="44.5,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFFFFFFF">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M45.914,46L49.45,42.464C49.841,42.073 49.841,41.441 49.45,41.05 49.059,40.659 48.427,40.659 48.036,41.05L44.5,44.586 40.964,41.05C40.573,40.659 39.941,40.659 39.55,41.05 39.159,41.441 39.159,42.073 39.55,42.464L43.086,46 39.55,49.536C39.159,49.927 39.159,50.559 39.55,50.95 39.745,51.145 40.001,51.243 40.257,51.243 40.513,51.243 40.769,51.145 40.964,50.95L44.5,47.414 48.036,50.95C48.231,51.145 48.487,51.243 48.743,51.243 48.999,51.243 49.255,51.145 49.45,50.95 49.841,50.559 49.841,49.927 49.45,49.536L45.914,46z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="ConnectIcon">
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup>
<DrawingGroup.ClipGeometry>
<RectangleGeometry Rect="0,0,59,59" />
</DrawingGroup.ClipGeometry>
<DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FF556080">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,19L3.608,19C1.616,19,0,17.384,0,15.392L0,3.608C0,1.616,1.616,0,3.608,0L54.391,0C56.384,0,58,1.616,58,3.608L58,15.391C58,17.384,56.384,19,54.392,19z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF38454F">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,38L3.608,38C1.616,38,0,36.384,0,34.392L0,22.608C0,20.616,1.616,19,3.608,19L54.391,19C56.384,19,58,20.616,58,22.608L58,34.391C58,36.384,56.384,38,54.392,38z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF556080">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,57L3.608,57C1.616,57,0,55.384,0,53.392L0,41.608C0,39.616,1.616,38,3.608,38L54.391,38C56.384,38,58,39.616,58,41.608L58,53.391C58,55.384,56.384,57,54.392,57z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,9.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,28.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,47.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FF26B999">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="12" RadiusY="12" Center="47,47" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFFFFFFF">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M53.571,41.179C53.116,40.863,52.494,40.975,52.179,41.429L46.583,49.469 42.634,46.227C42.208,45.876 41.577,45.939 41.227,46.366 40.876,46.793 40.938,47.423 41.366,47.773L46.152,51.702C46.332,51.849 46.556,51.929 46.786,51.929 46.831,51.929 46.877,51.926 46.923,51.92 47.199,51.881 47.447,51.73 47.607,51.501L53.821,42.572C54.136,42.118,54.024,41.495,53.571,41.179z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
<DrawingImage x:Key="DisconnectIcon">
<DrawingImage.Drawing>
<DrawingGroup>
<DrawingGroup>
<DrawingGroup.ClipGeometry>
<RectangleGeometry Rect="0,0,59,59" />
</DrawingGroup.ClipGeometry>
<DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FF556080">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,19L3.608,19C1.616,19,0,17.384,0,15.392L0,3.608C0,1.616,1.616,0,3.608,0L54.391,0C56.384,0,58,1.616,58,3.608L58,15.391C58,17.384,56.384,19,54.392,19z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF38454F">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,38L3.608,38C1.616,38,0,36.384,0,34.392L0,22.608C0,20.616,1.616,19,3.608,19L54.391,19C56.384,19,58,20.616,58,22.608L58,34.391C58,36.384,56.384,38,54.392,38z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF556080">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,57L3.608,57C1.616,57,0,55.384,0,53.392L0,41.608C0,39.616,1.616,38,3.608,38L54.391,38C56.384,38,58,39.616,58,41.608L58,53.391C58,55.384,56.384,57,54.392,57z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,9.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,28.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,47.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FFED7161">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="12" RadiusY="12" Center="47,47" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFFFFFFF">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M48.414,47L51.95,43.464C52.341,43.073 52.341,42.441 51.95,42.05 51.559,41.659 50.927,41.659 50.536,42.05L47,45.586 43.464,42.05C43.073,41.659 42.441,41.659 42.05,42.05 41.659,42.441 41.659,43.073 42.05,43.464L45.586,47 42.05,50.536C41.659,50.927 41.659,51.559 42.05,51.95 42.245,52.145 42.501,52.243 42.757,52.243 43.013,52.243 43.269,52.145 43.464,51.95L47,48.414 50.536,51.95C50.731,52.145 50.987,52.243 51.243,52.243 51.499,52.243 51.755,52.145 51.95,51.95 52.341,51.559 52.341,50.927 51.95,50.536L48.414,47z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</ResourceDictionary>

View File

@ -1,60 +0,0 @@
<DrawingGroup xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DrawingGroup x:Name="DrawingLayer">
<DrawingGroup.ClipGeometry>
<RectangleGeometry Rect="0,0,58,58" />
</DrawingGroup.ClipGeometry>
<DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FFEFEBDE">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M46.5,14L46.5,14 32.5,0 1.5,0 1.5,58 46.5,58z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<DrawingGroup>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M11.5,23L36.5,23C37.052,23 37.5,22.553 37.5,22 37.5,21.447 37.052,21 36.5,21L11.5,21C10.948,21 10.5,21.447 10.5,22 10.5,22.553 10.948,23 11.5,23z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M11.5,15L21.5,15C22.052,15 22.5,14.553 22.5,14 22.5,13.447 22.052,13 21.5,13L11.5,13C10.948,13 10.5,13.447 10.5,14 10.5,14.553 10.948,15 11.5,15z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M36.5,29L11.5,29C10.948,29 10.5,29.447 10.5,30 10.5,30.553 10.948,31 11.5,31L36.5,31C37.052,31 37.5,30.553 37.5,30 37.5,29.447 37.052,29 36.5,29z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M36.5,37L11.5,37C10.948,37 10.5,37.447 10.5,38 10.5,38.553 10.948,39 11.5,39L36.5,39C37.052,39 37.5,38.553 37.5,38 37.5,37.447 37.052,37 36.5,37z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M36.5,45L11.5,45C10.948,45 10.5,45.447 10.5,46 10.5,46.553 10.948,47 11.5,47L36.5,47C37.052,47 37.5,46.553 37.5,46 37.5,45.447 37.052,45 36.5,45z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
<GeometryDrawing Brush="#FFD5D0BB">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M32.5,0L32.5,0 32.5,14 46.5,14z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FFED7161">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="12" RadiusY="12" Center="44.5,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFFFFFFF">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M45.914,46L49.45,42.464C49.841,42.073 49.841,41.441 49.45,41.05 49.059,40.659 48.427,40.659 48.036,41.05L44.5,44.586 40.964,41.05C40.573,40.659 39.941,40.659 39.55,41.05 39.159,41.441 39.159,42.073 39.55,42.464L43.086,46 39.55,49.536C39.159,49.927 39.159,50.559 39.55,50.95 39.745,51.145 40.001,51.243 40.257,51.243 40.513,51.243 40.769,51.145 40.964,50.95L44.5,47.414 48.036,50.95C48.231,51.145 48.487,51.243 48.743,51.243 48.999,51.243 49.255,51.145 49.45,50.95 49.841,50.559 49.841,49.927 49.45,49.536L45.914,46z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>

View File

@ -1,203 +0,0 @@
<DrawingGroup xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DrawingGroup x:Name="DrawingLayer">
<DrawingGroup.ClipGeometry>
<RectangleGeometry Rect="0,0,59,59" />
</DrawingGroup.ClipGeometry>
<DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FF556080">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,19L3.608,19C1.616,19,0,17.384,0,15.392L0,3.608C0,1.616,1.616,0,3.608,0L54.391,0C56.384,0,58,1.616,58,3.608L58,15.391C58,17.384,56.384,19,54.392,19z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF38454F">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,38L3.608,38C1.616,38,0,36.384,0,34.392L0,22.608C0,20.616,1.616,19,3.608,19L54.391,19C56.384,19,58,20.616,58,22.608L58,34.391C58,36.384,56.384,38,54.392,38z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF556080">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,57L3.608,57C1.616,57,0,55.384,0,53.392L0,41.608C0,39.616,1.616,38,3.608,38L54.391,38C56.384,38,58,39.616,58,41.608L58,53.391C58,55.384,56.384,57,54.392,57z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,9.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,28.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,47.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FF26B999">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="12" RadiusY="12" Center="47,47" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFFFFFFF">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M53.571,41.179C53.116,40.863,52.494,40.975,52.179,41.429L46.583,49.469 42.634,46.227C42.208,45.876 41.577,45.939 41.227,46.366 40.876,46.793 40.938,47.423 41.366,47.773L46.152,51.702C46.332,51.849 46.556,51.929 46.786,51.929 46.831,51.929 46.877,51.926 46.923,51.92 47.199,51.881 47.447,51.73 47.607,51.501L53.821,42.572C54.136,42.118,54.024,41.495,53.571,41.179z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>

View File

@ -1,203 +0,0 @@
<DrawingGroup xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<DrawingGroup x:Name="DrawingLayer">
<DrawingGroup.ClipGeometry>
<RectangleGeometry Rect="0,0,59,59" />
</DrawingGroup.ClipGeometry>
<DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FF556080">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,19L3.608,19C1.616,19,0,17.384,0,15.392L0,3.608C0,1.616,1.616,0,3.608,0L54.391,0C56.384,0,58,1.616,58,3.608L58,15.391C58,17.384,56.384,19,54.392,19z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF38454F">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,38L3.608,38C1.616,38,0,36.384,0,34.392L0,22.608C0,20.616,1.616,19,3.608,19L54.391,19C56.384,19,58,20.616,58,22.608L58,34.391C58,36.384,56.384,38,54.392,38z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF556080">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M54.392,57L3.608,57C1.616,57,0,55.384,0,53.392L0,41.608C0,39.616,1.616,38,3.608,38L54.391,38C56.384,38,58,39.616,58,41.608L58,53.391C58,55.384,56.384,57,54.392,57z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,9.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,8" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,11" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,28.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,27" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="3.5" RadiusY="3.5" Center="9.5,47.5" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="49,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="45,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="51,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="47,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="41,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="43,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="37,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="39,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="33,46" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FF8697CB">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="1" RadiusY="1" Center="35,49" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
<DrawingGroup>
<GeometryDrawing Brush="#FFED7161">
<GeometryDrawing.Geometry>
<EllipseGeometry RadiusX="12" RadiusY="12" Center="47,47" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
<GeometryDrawing Brush="#FFFFFFFF">
<GeometryDrawing.Geometry>
<PathGeometry FillRule="Nonzero" Figures="M48.414,47L51.95,43.464C52.341,43.073 52.341,42.441 51.95,42.05 51.559,41.659 50.927,41.659 50.536,42.05L47,45.586 43.464,42.05C43.073,41.659 42.441,41.659 42.05,42.05 41.659,42.441 41.659,43.073 42.05,43.464L45.586,47 42.05,50.536C41.659,50.927 41.659,51.559 42.05,51.95 42.245,52.145 42.501,52.243 42.757,52.243 43.013,52.243 43.269,52.145 43.464,51.95L47,48.414 50.536,51.95C50.731,52.145 50.987,52.243 51.243,52.243 51.499,52.243 51.755,52.145 51.95,51.95 52.341,51.559 52.341,50.927 51.95,50.536L48.414,47z" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>
</DrawingGroup>

View File

@ -1,25 +0,0 @@
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace PettingZoo.Infrastructure
{
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void RaiseOtherPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}

View File

@ -1,81 +0,0 @@
using System;
using System.Windows.Input;
namespace PettingZoo.Infrastructure
{
public class DelegateCommand<T> : ICommand
{
private readonly Func<T, bool> canExecute;
private readonly Action<T> execute;
public event EventHandler CanExecuteChanged;
public DelegateCommand(Action<T> execute) : this(execute, null)
{
}
public DelegateCommand(Action<T> execute, Func<T, bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return canExecute == null || canExecute((T)parameter);
}
public void Execute(object parameter)
{
execute((T)parameter);
}
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}
}
public class DelegateCommand : ICommand
{
private readonly Func<bool> canExecute;
private readonly Action execute;
public event EventHandler CanExecuteChanged;
public DelegateCommand(Action execute) : this(execute, null) { }
public DelegateCommand(Action execute, Func<bool> canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object parameter)
{
return canExecute == null || canExecute();
}
public void Execute(object parameter)
{
execute();
}
public void RaiseCanExecuteChanged()
{
if (CanExecuteChanged != null)
CanExecuteChanged(this, EventArgs.Empty);
}
}
}

View File

@ -1,14 +0,0 @@
namespace PettingZoo.Model
{
public class ConnectionInfo
{
public string Host { get; set; }
public string VirtualHost { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Exchange { get; set; }
public string RoutingKey { get; set; }
}
}

View File

@ -1,46 +0,0 @@
using System;
namespace PettingZoo.Model
{
public enum ConnectionStatus
{
Disconnected,
Connecting,
Connected,
Error
}
public class StatusChangedEventArgs : EventArgs
{
public ConnectionStatus Status { get; private set; }
public string Context { get; private set; }
public StatusChangedEventArgs(ConnectionStatus status, string context)
{
Status = status;
Context = context;
}
}
public class MessageReceivedEventArgs : EventArgs
{
public MessageInfo MessageInfo { get; private set; }
public MessageReceivedEventArgs(MessageInfo messageInfo)
{
MessageInfo = messageInfo;
}
}
public interface IConnection : IDisposable
{
event EventHandler<StatusChangedEventArgs> StatusChanged;
event EventHandler<MessageReceivedEventArgs> MessageReceived;
}
}

View File

@ -1,7 +0,0 @@
namespace PettingZoo.Model
{
public interface IConnectionFactory
{
IConnection CreateConnection(ConnectionInfo connectionInfo);
}
}

View File

@ -1,7 +0,0 @@
namespace PettingZoo.Model
{
public interface IConnectionInfoBuilder
{
ConnectionInfo Build();
}
}

View File

@ -1,31 +0,0 @@
using System;
using System.Collections.Generic;
namespace PettingZoo.Model
{
public class MessageInfo
{
public DateTime Timestamp { get; set; }
public string Exchange { get; set; }
public string RoutingKey { get; set; }
public byte[] Body { get; set; }
public Dictionary<string, string> Properties;
public string ContentType
{
get
{
return Properties != null && Properties.ContainsKey(RabbitMQProperties.ContentType)
? Properties[RabbitMQProperties.ContentType]
: "";
}
}
public MessageInfo()
{
Timestamp = DateTime.Now;
}
}
}

View File

@ -1,190 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using PettingZoo.Properties;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace PettingZoo.Model
{
public class RabbitMQClientConnection : IConnection
{
private const int ConnectRetryDelay = 5000;
private readonly CancellationTokenSource connectionTaskToken;
private RabbitMQ.Client.IConnection connection;
private IModel model;
public event EventHandler<StatusChangedEventArgs> StatusChanged;
public event EventHandler<MessageReceivedEventArgs> MessageReceived;
public RabbitMQClientConnection(ConnectionInfo connectionInfo)
{
connectionTaskToken = new CancellationTokenSource();
var connectionToken = connectionTaskToken.Token;
Task.Factory.StartNew(() => TryConnection(connectionInfo, connectionToken), connectionToken);
}
public void Dispose()
{
connectionTaskToken.Cancel();
if (model != null)
{
model.Dispose();
model = null;
}
if (connection != null)
{
connection.Dispose();
connection = null;
}
StatusChanged = null;
MessageReceived = null;
}
private void TryConnection(ConnectionInfo connectionInfo, CancellationToken cancellationToken)
{
var factory = new ConnectionFactory
{
HostName = connectionInfo.Host,
Port = connectionInfo.Port,
VirtualHost = connectionInfo.VirtualHost,
UserName = connectionInfo.Username,
Password = connectionInfo.Password
};
var statusContext = String.Format("{0}:{1}{2}", connectionInfo.Host, connectionInfo.Port, connectionInfo.VirtualHost);
while (!cancellationToken.IsCancellationRequested)
{
DoStatusChanged(ConnectionStatus.Connecting, statusContext);
try
{
connection = factory.CreateConnection();
model = connection.CreateModel();
var queueName = model.QueueDeclare().QueueName;
model.QueueBind(queueName, connectionInfo.Exchange, connectionInfo.RoutingKey);
var consumer = new EventingBasicConsumer(model);
consumer.Received += ClientReceived;
model.BasicConsume(queueName, true, consumer);
DoStatusChanged(ConnectionStatus.Connected, statusContext);
break;
}
catch (Exception e)
{
DoStatusChanged(ConnectionStatus.Error, e.Message);
Task.Delay(ConnectRetryDelay, cancellationToken).Wait(cancellationToken);
}
}
}
private void ClientReceived(object sender, BasicDeliverEventArgs args)
{
if (MessageReceived == null)
return;
MessageReceived(this, new MessageReceivedEventArgs(
new MessageInfo
{
Exchange = args.Exchange,
RoutingKey = args.RoutingKey,
Body = args.Body,
Properties = ConvertProperties(args.BasicProperties)
}
));
}
private void DoStatusChanged(ConnectionStatus status, string context = null)
{
if (StatusChanged != null)
StatusChanged(this, new StatusChangedEventArgs(status, context));
}
private static Dictionary<string, string> ConvertProperties(IBasicProperties basicProperties)
{
var properties = new Dictionary<string, string>();
if (basicProperties.IsDeliveryModePresent())
{
string deliveryMode;
switch (basicProperties.DeliveryMode)
{
case 1:
deliveryMode = Resources.DeliveryModeNonPersistent;
break;
case 2:
deliveryMode = Resources.DeliveryModePersistent;
break;
default:
deliveryMode = basicProperties.DeliveryMode.ToString(CultureInfo.InvariantCulture);
break;
}
properties.Add(RabbitMQProperties.DeliveryMode, deliveryMode);
}
if (basicProperties.IsContentTypePresent())
properties.Add(RabbitMQProperties.ContentType, basicProperties.ContentType);
if (basicProperties.IsContentEncodingPresent())
properties.Add(RabbitMQProperties.ContentEncoding, basicProperties.ContentEncoding);
if (basicProperties.IsPriorityPresent())
properties.Add(RabbitMQProperties.Priority, basicProperties.Priority.ToString(CultureInfo.InvariantCulture));
if (basicProperties.IsCorrelationIdPresent())
properties.Add(RabbitMQProperties.Priority, basicProperties.CorrelationId);
if (basicProperties.IsReplyToPresent())
properties.Add(RabbitMQProperties.ReplyTo, basicProperties.ReplyTo);
if (basicProperties.IsExpirationPresent())
properties.Add(RabbitMQProperties.Expiration, basicProperties.Expiration);
if (basicProperties.IsMessageIdPresent())
properties.Add(RabbitMQProperties.MessageId, basicProperties.MessageId);
if (basicProperties.IsTimestampPresent())
properties.Add(RabbitMQProperties.Timestamp, basicProperties.Timestamp.UnixTime.ToString(CultureInfo.InvariantCulture));
if (basicProperties.IsTypePresent())
properties.Add(RabbitMQProperties.Type, basicProperties.Type);
if (basicProperties.IsUserIdPresent())
properties.Add(RabbitMQProperties.UserId, basicProperties.UserId);
if (basicProperties.IsAppIdPresent())
properties.Add(RabbitMQProperties.UserId, basicProperties.AppId);
if (basicProperties.IsClusterIdPresent())
properties.Add(RabbitMQProperties.ClusterId, basicProperties.ClusterId);
foreach (var header in basicProperties.Headers)
properties.Add(header.Key, Encoding.UTF8.GetString((byte[])header.Value));
return properties;
}
}
}

View File

@ -1,10 +0,0 @@
namespace PettingZoo.Model
{
public class RabbitMQClientConnectionFactory : IConnectionFactory
{
public IConnection CreateConnection(ConnectionInfo connectionInfo)
{
return new RabbitMQClientConnection(connectionInfo);
}
}
}

View File

@ -0,0 +1,21 @@
namespace PettingZoo.Core.Connection
{
public class ConnectionParams
{
public string Host { get; }
public string VirtualHost { get; }
public int Port { get; }
public string Username { get; }
public string Password { get; }
public ConnectionParams(string host, string virtualHost, int port, string username, string password)
{
Host = host;
VirtualHost = virtualHost;
Port = port;
Username = username;
Password = password;
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using System.Threading.Tasks;
namespace PettingZoo.Core.Connection
{
public interface IConnection : IAsyncDisposable
{
event EventHandler<StatusChangedEventArgs> StatusChanged;
ISubscriber Subscribe(string exchange, string routingKey);
Task Publish(MessageInfo messageInfo);
}
public enum ConnectionStatus
{
Disconnected,
Connecting,
Connected,
Error
}
public class StatusChangedEventArgs : EventArgs
{
public ConnectionStatus Status { get; }
public string? Context { get; }
public StatusChangedEventArgs(ConnectionStatus status, string? context)
{
Status = status;
Context = context;
}
}
}

View File

@ -0,0 +1,7 @@
namespace PettingZoo.Core.Connection
{
public interface IConnectionFactory
{
IConnection CreateConnection(ConnectionParams connectionInfo);
}
}

View File

@ -0,0 +1,26 @@
using System;
namespace PettingZoo.Core.Connection
{
public interface ISubscriber : IAsyncDisposable
{
string Exchange {get; }
string RoutingKey { get; }
event EventHandler<MessageReceivedEventArgs>? MessageReceived;
void Start();
}
public class MessageReceivedEventArgs : EventArgs
{
public MessageInfo MessageInfo { get; }
public MessageReceivedEventArgs(MessageInfo messageInfo)
{
MessageInfo = messageInfo;
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
namespace PettingZoo.Core.Connection
{
public class MessageInfo
{
public DateTime Timestamp { get; }
public string Exchange { get; }
public string RoutingKey { get; }
public byte[] Body { get; }
public IDictionary<string, string> Properties { get; }
public MessageInfo(string exchange, string routingKey, byte[] body, IDictionary<string, string> properties, DateTime timestamp)
{
Exchange = exchange;
RoutingKey = routingKey;
Body = body;
Properties = properties;
Timestamp = timestamp;
}
}
}

View File

@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>

View File

@ -3,11 +3,11 @@ using System.Collections.Generic;
using System.Text; using System.Text;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace PettingZoo.Model namespace PettingZoo.Core.Rendering
{ {
public class MessageBodyRenderer public class MessageBodyRenderer
{ {
public static Dictionary<string, Func<byte[], string>> ContentTypeHandlers = new Dictionary<string, Func<byte[], string>> public static Dictionary<string, Func<byte[], string>> ContentTypeHandlers = new()
{ {
{ "application/json", RenderJson } { "application/json", RenderJson }
}; };
@ -15,13 +15,11 @@ namespace PettingZoo.Model
public static string Render(byte[] body, string contentType = "") public static string Render(byte[] body, string contentType = "")
{ {
Func<byte[], string> handler; return ContentTypeHandlers.TryGetValue(contentType, out var handler)
? handler(body)
if (ContentTypeHandlers.TryGetValue(contentType, out handler)) : Encoding.UTF8.GetString(body);
return handler(body);
// ToDo hex output if required // ToDo hex output if required
return Encoding.UTF8.GetString(body);
} }

View File

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="RabbitMQ.Client" Version="6.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PettingZoo.Core\PettingZoo.Core.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,142 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using PettingZoo.Core.Connection;
using RabbitMQ.Client;
namespace PettingZoo.RabbitMQ
{
public class RabbitMQClientConnection : Core.Connection.IConnection
{
private const int ConnectRetryDelay = 5000;
private readonly CancellationTokenSource connectionTaskToken = new();
private readonly Task connectionTask;
private readonly object connectionLock = new();
private global::RabbitMQ.Client.IConnection? connection;
private IModel? model;
public event EventHandler<StatusChangedEventArgs>? StatusChanged;
public RabbitMQClientConnection(ConnectionParams connectionParams)
{
connectionTask = Task.Factory.StartNew(() => TryConnection(connectionParams, connectionTaskToken.Token), CancellationToken.None);
}
public async ValueTask DisposeAsync()
{
connectionTaskToken.Cancel();
if (!connectionTask.IsCompleted)
await connectionTask;
lock (connectionLock)
{
if (model != null)
{
model.Dispose();
model = null;
}
if (connection != null)
{
connection.Dispose();
connection = null;
}
}
}
public ISubscriber Subscribe(string exchange, string routingKey)
{
lock (connectionLock)
{
var subscriber = new RabbitMQClientSubscriber(model, exchange, routingKey);
if (model != null)
return subscriber;
void ConnectSubscriber(object? sender, StatusChangedEventArgs args)
{
if (args.Status != ConnectionStatus.Connected)
return;
lock (connectionLock)
{
if (model == null)
return;
subscriber.Connected(model);
}
StatusChanged -= ConnectSubscriber;
}
StatusChanged += ConnectSubscriber;
return subscriber;
}
}
public Task Publish(MessageInfo messageInfo)
{
if (model == null)
throw new InvalidOperationException("Not connected");
model.BasicPublish(messageInfo.Exchange, messageInfo.RoutingKey, false,
RabbitMQClientPropertiesConverter.Convert(messageInfo.Properties, model.CreateBasicProperties()),
messageInfo.Body);
return Task.CompletedTask;
}
private void TryConnection(ConnectionParams connectionParams, CancellationToken cancellationToken)
{
var factory = new ConnectionFactory
{
HostName = connectionParams.Host,
Port = connectionParams.Port,
VirtualHost = connectionParams.VirtualHost,
UserName = connectionParams.Username,
Password = connectionParams.Password
};
var statusContext = $"{connectionParams.Host}:{connectionParams.Port}{connectionParams.VirtualHost}";
while (!cancellationToken.IsCancellationRequested)
{
DoStatusChanged(ConnectionStatus.Connecting, statusContext);
try
{
connection = factory.CreateConnection();
model = connection.CreateModel();
DoStatusChanged(ConnectionStatus.Connected, statusContext);
break;
}
catch (Exception e)
{
DoStatusChanged(ConnectionStatus.Error, e.Message);
try
{
Task.Delay(ConnectRetryDelay, cancellationToken).Wait(cancellationToken);
}
catch (OperationCanceledException)
{
}
}
}
}
private void DoStatusChanged(ConnectionStatus status, string? context = null)
{
StatusChanged?.Invoke(this, new StatusChangedEventArgs(status, context));
}
}
}

View File

@ -0,0 +1,12 @@
using PettingZoo.Core.Connection;
namespace PettingZoo.RabbitMQ
{
public class RabbitMQClientConnectionFactory : IConnectionFactory
{
public IConnection CreateConnection(ConnectionParams connectionParams)
{
return new RabbitMQClientConnection(connectionParams);
}
}
}

View File

@ -0,0 +1,138 @@
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using RabbitMQ.Client;
namespace PettingZoo.RabbitMQ
{
public static class RabbitMQClientPropertiesConverter
{
public static IDictionary<string, string> Convert(IBasicProperties basicProperties)
{
var properties = new Dictionary<string, string>();
if (basicProperties.IsDeliveryModePresent())
properties.Add(RabbitMQProperties.DeliveryMode, basicProperties.DeliveryMode.ToString(CultureInfo.InvariantCulture));
if (basicProperties.IsContentTypePresent())
properties.Add(RabbitMQProperties.ContentType, basicProperties.ContentType);
if (basicProperties.IsContentEncodingPresent())
properties.Add(RabbitMQProperties.ContentEncoding, basicProperties.ContentEncoding);
if (basicProperties.IsPriorityPresent())
properties.Add(RabbitMQProperties.Priority, basicProperties.Priority.ToString(CultureInfo.InvariantCulture));
if (basicProperties.IsCorrelationIdPresent())
properties.Add(RabbitMQProperties.Priority, basicProperties.CorrelationId);
if (basicProperties.IsReplyToPresent())
properties.Add(RabbitMQProperties.ReplyTo, basicProperties.ReplyTo);
if (basicProperties.IsExpirationPresent())
properties.Add(RabbitMQProperties.Expiration, basicProperties.Expiration);
if (basicProperties.IsMessageIdPresent())
properties.Add(RabbitMQProperties.MessageId, basicProperties.MessageId);
if (basicProperties.IsTimestampPresent())
properties.Add(RabbitMQProperties.Timestamp, basicProperties.Timestamp.UnixTime.ToString(CultureInfo.InvariantCulture));
if (basicProperties.IsTypePresent())
properties.Add(RabbitMQProperties.Type, basicProperties.Type);
if (basicProperties.IsUserIdPresent())
properties.Add(RabbitMQProperties.UserId, basicProperties.UserId);
if (basicProperties.IsAppIdPresent())
properties.Add(RabbitMQProperties.UserId, basicProperties.AppId);
if (basicProperties.IsClusterIdPresent())
properties.Add(RabbitMQProperties.ClusterId, basicProperties.ClusterId);
// ReSharper disable once InvertIf
if (basicProperties.Headers != null)
{
foreach (var (key, value) in basicProperties.Headers)
properties.Add(key, Encoding.UTF8.GetString((byte[]) value));
}
return properties;
}
public static IBasicProperties Convert(IDictionary<string, string> properties, IBasicProperties targetProperties)
{
foreach (var (key, value) in properties)
{
switch (key)
{
case RabbitMQProperties.DeliveryMode:
if (byte.TryParse(value, out var deliveryMode))
targetProperties.DeliveryMode = deliveryMode;
break;
case RabbitMQProperties.ContentType:
targetProperties.ContentType = value;
break;
case RabbitMQProperties.ContentEncoding:
targetProperties.ContentEncoding = value;
break;
case RabbitMQProperties.Priority:
if (byte.TryParse(value, out var priority))
targetProperties.Priority = priority;
break;
case RabbitMQProperties.CorrelationId:
targetProperties.CorrelationId = value;
break;
case RabbitMQProperties.ReplyTo:
targetProperties.ReplyTo = value;
break;
case RabbitMQProperties.Expiration:
targetProperties.Expiration = value;
break;
case RabbitMQProperties.MessageId:
targetProperties.MessageId = value;
break;
case RabbitMQProperties.Timestamp:
if (long.TryParse(value, out var timestamp))
targetProperties.Timestamp = new AmqpTimestamp(timestamp);
break;
case RabbitMQProperties.Type:
targetProperties.Type = value;
break;
case RabbitMQProperties.UserId:
targetProperties.UserId = value;
break;
case RabbitMQProperties.AppId:
targetProperties.AppId = value;
break;
case RabbitMQProperties.ClusterId:
targetProperties.ClusterId = value;
break;
default:
targetProperties.Headers ??= new Dictionary<string, object>();
targetProperties.Headers.Add(key, Encoding.UTF8.GetBytes(value));
break;
}
}
return targetProperties;
}
}
}

View File

@ -0,0 +1,79 @@
using System;
using System.Threading.Tasks;
using PettingZoo.Core.Connection;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace PettingZoo.RabbitMQ
{
public class RabbitMQClientSubscriber : ISubscriber
{
private IModel? model;
private string? consumerTag;
private bool started;
public string Exchange { get; }
public string RoutingKey { get; }
public event EventHandler<MessageReceivedEventArgs>? MessageReceived;
public RabbitMQClientSubscriber(IModel? model, string exchange, string routingKey)
{
this.model = model;
Exchange = exchange;
RoutingKey = routingKey;
}
public ValueTask DisposeAsync()
{
if (model != null && consumerTag != null && model.IsOpen)
model.BasicCancelNoWait(consumerTag);
return default;
}
public void Start()
{
started = true;
if (model == null)
return;
var queueName = model.QueueDeclare().QueueName;
model.QueueBind(queueName, Exchange, RoutingKey);
var consumer = new EventingBasicConsumer(model);
consumer.Received += ClientReceived;
consumerTag = model.BasicConsume(queueName, true, consumer);
}
public void Connected(IModel newModel)
{
model = newModel;
if (started)
Start();
}
private void ClientReceived(object? sender, BasicDeliverEventArgs args)
{
MessageReceived?.Invoke(this, new MessageReceivedEventArgs(
new MessageInfo(
args.Exchange,
args.RoutingKey,
args.Body.ToArray(),
RabbitMQClientPropertiesConverter.Convert(args.BasicProperties),
args.BasicProperties.Timestamp.UnixTime > 0
? DateTimeOffset.FromUnixTimeSeconds(args.BasicProperties.Timestamp.UnixTime).LocalDateTime
: DateTime.Now
)
));
}
}
}

View File

@ -1,6 +1,6 @@
namespace PettingZoo.Model namespace PettingZoo.RabbitMQ
{ {
static class RabbitMQProperties public static class RabbitMQProperties
{ {
public const string ContentType = "content-type"; public const string ContentType = "content-type";
public const string ContentEncoding = "content-encoding"; public const string ContentEncoding = "content-encoding";

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace PettingZoo.RabbitMQ
{
public static class RabbitMQPropertiesExtensions
{
public static string ContentType(this IDictionary<string, string> properties)
{
return properties.TryGetValue(RabbitMQProperties.ContentType, out var value)
? value
: "";
}
}
}

View File

@ -1,188 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{24819D09-C747-4356-B686-D9DE9CAA6F59}</ProjectGuid>
<OutputType>WinExe</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>PettingZoo</RootNamespace>
<AssemblyName>PettingZoo</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>0</ApplicationRevision>
<ApplicationVersion>1.0.0.%2a</ApplicationVersion>
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<StartupObject>
</StartupObject>
</PropertyGroup>
<PropertyGroup>
<ApplicationIcon>PettingZoo.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Reference Include="AutoMapper, Version=4.2.1.0, Culture=neutral, PublicKeyToken=be96cd2c38ef1005, processorArchitecture=MSIL">
<HintPath>packages\AutoMapper.4.2.1\lib\net45\AutoMapper.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="Newtonsoft.Json, Version=8.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>packages\Newtonsoft.Json.8.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="RabbitMQ.Client, Version=3.6.2.0, Culture=neutral, PublicKeyToken=89e7d7c5feba84ce, processorArchitecture=MSIL">
<HintPath>packages\RabbitMQ.Client.3.6.2\lib\net45\RabbitMQ.Client.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="SimpleInjector, Version=3.1.5.0, Culture=neutral, PublicKeyToken=984cb50dea722e99, processorArchitecture=MSIL">
<HintPath>packages\SimpleInjector.3.1.5\lib\net45\SimpleInjector.dll</HintPath>
<Private>True</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Page Include="Icons.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Style.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\ConnectionWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="View\MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="Infrastructure\DelegateCommand.cs" />
<Compile Include="Infrastructure\BaseViewModel.cs" />
<Compile Include="Infrastructure\GridLayout.cs" />
<Compile Include="Infrastructure\ListBoxAutoScroll.cs" />
<Compile Include="Infrastructure\PasswordBoxAssistant.cs" />
<Compile Include="Model\UserSettings.cs" />
<Compile Include="Model\IConnection.cs" />
<Compile Include="Model\IConnectionFactory.cs" />
<Compile Include="Model\IConnectionInfoBuilder.cs" />
<Compile Include="Model\ConnectionInfo.cs" />
<Compile Include="Model\MessageBodyRenderer.cs" />
<Compile Include="Model\MessageInfo.cs" />
<Compile Include="Model\RabbitMQClientConnection.cs" />
<Compile Include="Model\RabbitMQClientConnectionFactory.cs" />
<Compile Include="Model\RabbitMQProperties.cs" />
<Compile Include="ViewModel\ConnectionViewModel.cs" />
<Compile Include="ViewModel\MainViewModel.cs" />
<Compile Include="View\ConnectionWindow.xaml.cs">
<DependentUpon>ConnectionWindow.xaml</DependentUpon>
</Compile>
<Compile Include="View\MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="packages.config" />
<AppDesigner Include="Properties\" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.5">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.5 %28x86 and x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Client.3.5">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1 Client Profile</ProductName>
<Install>false</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<ItemGroup>
<Resource Include="PettingZoo.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@ -1,9 +1,18 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013 # Visual Studio Version 16
VisualStudioVersion = 12.0.40629.0 VisualStudioVersion = 16.0.31911.196
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo", "PettingZoo.csproj", "{24819D09-C747-4356-B686-D9DE9CAA6F59}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PettingZoo", "PettingZoo\PettingZoo.csproj", "{24819D09-C747-4356-B686-D9DE9CAA6F59}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A64E3FB8-7606-4A05-BF10-D83FD0E80D2D}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.Core", "PettingZoo.Core\PettingZoo.Core.csproj", "{AD20CA14-6272-4C50-819D-F9FE6A963DB1}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PettingZoo.RabbitMQ", "PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj", "{220149F3-A8D6-44ED-B3B6-DFE506EB018A}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -15,8 +24,19 @@ Global
{24819D09-C747-4356-B686-D9DE9CAA6F59}.Debug|Any CPU.Build.0 = Debug|Any CPU {24819D09-C747-4356-B686-D9DE9CAA6F59}.Debug|Any CPU.Build.0 = Debug|Any CPU
{24819D09-C747-4356-B686-D9DE9CAA6F59}.Release|Any CPU.ActiveCfg = Release|Any CPU {24819D09-C747-4356-B686-D9DE9CAA6F59}.Release|Any CPU.ActiveCfg = Release|Any CPU
{24819D09-C747-4356-B686-D9DE9CAA6F59}.Release|Any CPU.Build.0 = Release|Any CPU {24819D09-C747-4356-B686-D9DE9CAA6F59}.Release|Any CPU.Build.0 = Release|Any CPU
{AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD20CA14-6272-4C50-819D-F9FE6A963DB1}.Release|Any CPU.Build.0 = Release|Any CPU
{220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{220149F3-A8D6-44ED-B3B6-DFE506EB018A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {07CE270E-9E57-49CD-8D5C-79C7B7A98517}
EndGlobalSection
EndGlobal EndGlobal

View File

@ -1,12 +1,12 @@
<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="PettingZoo.App" x:Class="PettingZoo.App"
Startup="ApplicationStartup"> ShutdownMode="OnMainWindowClose"
DispatcherUnhandledException="App_OnDispatcherUnhandledException">
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Style.xaml"/> <ResourceDictionary Source="Style.xaml"/>
<ResourceDictionary Source="Icons.xaml"/>
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>

13
PettingZoo/App.xaml.cs Normal file
View File

@ -0,0 +1,13 @@
using System.Windows;
using System.Windows.Threading;
namespace PettingZoo
{
public partial class App
{
private void App_OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{
_ = MessageBox.Show($"Unhandled exception: {e.Exception.Message}", "Petting Zoo - Exception", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58.707 58.707" style="enable-background:new 0 0 58.707 58.707;" xml:space="preserve">
<g>
<g>
<polygon style="fill:#EFEBDE;" points="46.072,14 32.072,0 1.072,0 1.072,58 46.072,58 "/>
<g>
<path style="fill:#D5D0BB;" d="M11.072,23h25c0.552,0,1-0.447,1-1s-0.448-1-1-1h-25c-0.552,0-1,0.447-1,1S10.52,23,11.072,23z"/>
<path style="fill:#D5D0BB;" d="M11.072,15h10c0.552,0,1-0.447,1-1s-0.448-1-1-1h-10c-0.552,0-1,0.447-1,1S10.52,15,11.072,15z"/>
<path style="fill:#D5D0BB;" d="M36.072,29h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S36.624,29,36.072,29z"/>
<path style="fill:#D5D0BB;" d="M36.072,37h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S36.624,37,36.072,37z"/>
<path style="fill:#D5D0BB;" d="M36.072,45h-25c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S36.624,45,36.072,45z"/>
</g>
<polygon style="fill:#D5D0BB;" points="32.072,0 32.072,14 46.072,14 "/>
</g>
<g>
<polygon style="fill:#EDDCC7;" points="36.201,49.214 36.194,49.222 34.205,56.511 38.852,51.865 "/>
<path style="fill:#D75A4A;" d="M55.451,35.266l-1.247-1.247c-0.775-0.775-2.032-0.775-2.807,0L47.815,37.6l2.651,2.651
L55.451,35.266z"/>
<rect x="41.459" y="36.521" transform="matrix(0.7071 0.7071 -0.7071 0.7071 44.3228 -17.5395)" style="fill:#F29C21;" width="3.749" height="16.424"/>
<polygon style="fill:#D6C4B1;" points="41.85,54.879 41.858,54.871 38.852,51.865 34.205,56.511 34.072,57 "/>
<path style="fill:#A34740;" d="M53.472,43.257l3.582-3.582c0.775-0.775,0.775-2.032,0-2.807l-1.602-1.602l-4.985,4.985
L53.472,43.257z"/>
<rect x="44.036" y="39.349" transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 45.1717 113.8333)" style="fill:#E18C25;" width="4.251" height="16.424"/>
<path style="fill:#5E5E5E;" d="M33.365,58.707c-0.256,0-0.512-0.098-0.707-0.293c-0.391-0.391-0.391-1.023,0-1.414l2.207-2.207
c0.391-0.391,1.023-0.391,1.414,0s0.391,1.023,0,1.414l-2.207,2.207C33.877,58.609,33.621,58.707,33.365,58.707z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58 58" style="enable-background:new 0 0 58 58;" xml:space="preserve">
<g>
<g>
<polygon style="fill:#DCD6CD;" points="0,5.5 0,44.5 28,44.5 56,44.5 56,5.5 "/>
<path style="fill:#E8E3D9;" d="M30.965,27.607c-1.637,1.462-4.292,1.462-5.93,0l-2.087-1.843C16.419,31.591,0,44.5,0,44.5h21.607
h12.787H56c0,0-16.419-12.909-22.948-18.736L30.965,27.607z"/>
<path style="fill:#EFEBDE;" d="M0,5.5l25.035,22.107c1.637,1.462,4.292,1.462,5.93,0L56,5.5H0z"/>
</g>
<g>
<rect x="36" y="30.5" style="fill:#48A0DC;" width="22" height="22"/>
<rect x="46" y="36.5" style="fill:#FFFFFF;" width="2" height="16"/>
<polygon style="fill:#FFFFFF;" points="52.293,43.207 47,37.914 41.707,43.207 40.293,41.793 47,35.086 53.707,41.793 "/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 58.195 58.195" style="enable-background:new 0 0 58.195 58.195;" xml:space="preserve">
<g>
<g>
<polygon style="fill:#EFEBDE;" points="45,14.097 31,0.097 0,0.097 0,58.097 45,58.097 "/>
<g>
<path style="fill:#D5D0BB;" d="M10,23.097h25c0.552,0,1-0.447,1-1s-0.448-1-1-1H10c-0.552,0-1,0.447-1,1S9.448,23.097,10,23.097z
"/>
<path style="fill:#D5D0BB;" d="M10,15.097h10c0.552,0,1-0.447,1-1s-0.448-1-1-1H10c-0.552,0-1,0.447-1,1S9.448,15.097,10,15.097z
"/>
<path style="fill:#D5D0BB;" d="M35,29.097H10c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S35.552,29.097,35,29.097z
"/>
<path style="fill:#D5D0BB;" d="M35,37.097H10c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S35.552,37.097,35,37.097z
"/>
<path style="fill:#D5D0BB;" d="M35,45.097H10c-0.552,0-1,0.447-1,1s0.448,1,1,1h25c0.552,0,1-0.447,1-1S35.552,45.097,35,45.097z
"/>
</g>
<polygon style="fill:#D5D0BB;" points="31,0.097 31,14.097 45,14.097 "/>
</g>
<g>
<path style="fill:#FFFFFF;" d="M57,48.289l-0.107,0.163c-7.121,10.876-18.773,10.876-25.893,0l0,0l0.107-0.163
C38.227,37.412,49.879,37.412,57,48.289L57,48.289z"/>
<circle style="fill:#556080;" cx="43.764" cy="46.007" r="5.909"/>
<path style="fill:#8697CB;" d="M43.947,57.609c-5.254,0-10.148-3.058-13.783-8.609l-0.358-0.547l0.465-0.711
c3.635-5.552,8.53-8.609,13.784-8.609c5.253,0,10.148,3.057,13.783,8.609l0.358,0.547l-0.465,0.711
C54.095,54.552,49.2,57.609,43.947,57.609z M32.203,48.448c3.206,4.624,7.356,7.161,11.744,7.161c4.436,0,8.63-2.594,11.85-7.317
c-3.206-4.624-7.356-7.161-11.743-7.161C39.617,41.132,35.423,43.725,32.203,48.448z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,112 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Authors>Mark van Renswoude</Authors>
<Product>Petting Zoo</Product>
<Description>Petting Zoo - a live RabbitMQ message viewer</Description>
<Copyright />
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<StartupObject>PettingZoo.Program</StartupObject>
<PackageIcon></PackageIcon>
<PackageIconUrl />
<ApplicationIcon>PettingZoo.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<None Remove="Images\PublishSend.svg" />
</ItemGroup>
<ItemGroup>
<Resource Include="Images\Clear.svg" />
<Resource Include="Images\Connect.svg" />
<Resource Include="Images\Disconnect.svg" />
<Resource Include="Images\Publish.svg" />
<Resource Include="Images\PublishSend.svg" />
<Resource Include="Images\Subscribe.svg" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="SharpVectors" Version="1.7.6" />
<PackageReference Include="SimpleInjector" Version="5.3.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PettingZoo.Core\PettingZoo.Core.csproj" />
<ProjectReference Include="..\PettingZoo.RabbitMQ\PettingZoo.RabbitMQ.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="UI\Connection\ConnectionWindowStrings.Designer.cs">
<DependentUpon>ConnectionWindowStrings.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Update="UI\Main\MainWindowStrings.Designer.cs">
<DependentUpon>MainWindowStrings.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Update="UI\Subscribe\SubscribeWindowStrings.Designer.cs">
<DependentUpon>SubscribeWindowStrings.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Update="UI\Tab\Publisher\PublisherViewStrings.Designer.cs">
<DependentUpon>PublisherViewStrings.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
<Compile Update="UI\Tab\Subscriber\SubscriberViewStrings.Designer.cs">
<DependentUpon>SubscriberViewStrings.resx</DependentUpon>
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="UI\Connection\ConnectionWindowStrings.resx">
<LastGenOutput>ConnectionWindowStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="UI\Main\MainWindowStrings.resx">
<LastGenOutput>MainWindowStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="UI\Subscribe\SubscribeWindowStrings.resx">
<LastGenOutput>SubscribeWindowStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="UI\Tab\Publisher\PublisherViewStrings.resx">
<LastGenOutput>PublisherViewStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<EmbeddedResource Update="UI\Tab\Subscriber\SubscriberViewStrings.resx">
<LastGenOutput>SubscriberViewStrings.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Page Update="UI\Connection\ConnectionWindow.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
<Page Update="UI\Main\MainWindow.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
<Page Update="UI\Subscribe\SubscribeWindow.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
<Page Update="UI\Tab\Publisher\PublisherView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
<Page Update="UI\Tab\Subscriber\SubscriberView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
</ItemGroup>
</Project>

View File

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@ -5,16 +5,22 @@ using System.Reflection;
using System.Windows; using System.Windows;
using System.Windows.Markup; using System.Windows.Markup;
using Newtonsoft.Json; using Newtonsoft.Json;
using PettingZoo.Model; using PettingZoo.Core.Connection;
using PettingZoo.View; using PettingZoo.RabbitMQ;
using PettingZoo.ViewModel; using PettingZoo.Settings;
using PettingZoo.UI.Connection;
using PettingZoo.UI.Main;
using PettingZoo.UI.Subscribe;
using PettingZoo.UI.Tab;
using PettingZoo.UI.Tab.Subscriber;
using SimpleInjector; using SimpleInjector;
namespace PettingZoo namespace PettingZoo
{ {
public partial class App public static class Program
{ {
public void ApplicationStartup(object sender, StartupEventArgs e) [STAThread]
public static void Main()
{ {
// WPF defaults to US for date formatting in bindings, this fixes it // WPF defaults to US for date formatting in bindings, this fixes it
FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata( FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(
@ -29,16 +35,17 @@ namespace PettingZoo
{ {
var container = new Container(); var container = new Container();
// See comments in RunApplication
container.Options.EnableAutoVerification = false;
container.RegisterSingleton(() => new UserSettings(new AppDataSettingsSerializer("Settings.json"))); container.RegisterSingleton(() => new UserSettings(new AppDataSettingsSerializer("Settings.json")));
container.Register<IConnectionFactory, RabbitMQClientConnectionFactory>(); container.Register<IConnectionFactory, RabbitMQClientConnectionFactory>();
container.Register<IConnectionInfoBuilder, WindowConnectionInfoBuilder>(); container.Register<IConnectionDialog, WindowConnectionDialog>();
container.Register<ISubscribeDialog, WindowSubscribeDialog>();
container.Register<ITabFactory, ViewTabFactory>();
container.Register<MainWindow>(); container.Register<MainWindow>();
container.Register<MainViewModel>();
// Note: don't run Verify! It'll create a MainWindow which will then become
// Application.Current.MainWindow and prevent the process from shutting down.
return container; return container;
} }
@ -46,10 +53,31 @@ namespace PettingZoo
private static void RunApplication(Container container) private static void RunApplication(Container container)
{ {
var mainWindow = container.GetInstance<MainWindow>(); try
mainWindow.Closed += (sender, args) => container.Dispose(); {
var app = new App();
app.InitializeComponent();
mainWindow.Show(); #if DEBUG
// Verify container after initialization to prevent issues loading the resource dictionaries
container.Verify();
// This causes the MainWindow and Windows properties to be populated however, which we don't want
// because then the app does not close properly when using OnMainWindowClose, so clean up the mess
app.MainWindow = null;
foreach (var window in app.Windows)
((Window)window).Close();
// All this is the reason we only perform verification in debug builds
#endif
var mainWindow = container.GetInstance<MainWindow>();
_ = app.Run(mainWindow);
}
catch (Exception)
{
// TODO Log the exception and exit
}
} }
@ -79,7 +107,7 @@ namespace PettingZoo
public void Write(UserSettings settings) public void Write(UserSettings settings)
{ {
Directory.CreateDirectory(path); _ = Directory.CreateDirectory(path);
File.WriteAllText(fullPath, JsonConvert.SerializeObject(settings, Formatting.Indented)); File.WriteAllText(fullPath, JsonConvert.SerializeObject(settings, Formatting.Indented));
} }
@ -87,10 +115,9 @@ namespace PettingZoo
private T GetProductInfo<T>() private T GetProductInfo<T>()
{ {
var attributes = GetType().Assembly.GetCustomAttributes(typeof(T), true); var attributes = GetType().Assembly.GetCustomAttributes(typeof(T), true);
if (attributes.Length == 0) return attributes.Length == 0
throw new Exception("Missing product information in assembly"); ? throw new Exception("Missing product information in assembly")
: (T) attributes[0];
return (T)attributes[0];
} }
} }
} }

View File

@ -1,4 +1,4 @@
namespace PettingZoo.Model namespace PettingZoo.Settings
{ {
public interface IUserSettingsSerializer public interface IUserSettingsSerializer
{ {
@ -15,6 +15,7 @@
public string LastUsername { get; set; } public string LastUsername { get; set; }
public string LastPassword { get; set; } public string LastPassword { get; set; }
//public bool LastSubscribe { get; set; }
public string LastExchange { get; set; } public string LastExchange { get; set; }
public string LastRoutingKey { get; set; } public string LastRoutingKey { get; set; }
@ -27,7 +28,7 @@
LastUsername = "guest"; LastUsername = "guest";
LastPassword = "guest"; LastPassword = "guest";
LastExchange = "amqp"; LastExchange = "";
LastRoutingKey = "#"; LastRoutingKey = "#";
} }
} }
@ -35,7 +36,7 @@
public class UserSettings public class UserSettings
{ {
public ConnectionWindowSettings ConnectionWindow { get; private set; } public ConnectionWindowSettings ConnectionWindow { get; }
private readonly IUserSettingsSerializer serializer; private readonly IUserSettingsSerializer serializer;

View File

@ -1,6 +1,6 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:infrastructure="clr-namespace:PettingZoo.Infrastructure"> xmlns:ui="clr-namespace:PettingZoo.UI">
<!-- Global styling --> <!-- Global styling -->
<Style x:Key="WindowStyle" TargetType="{x:Type Window}"> <Style x:Key="WindowStyle" TargetType="{x:Type Window}">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/> <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
@ -30,7 +30,7 @@
</Style> </Style>
<Style x:Key="Form" TargetType="{x:Type infrastructure:GridLayout}"> <Style x:Key="Form" TargetType="{x:Type ui:GridLayout}">
<Setter Property="ChildMargin" Value="4"/> <Setter Property="ChildMargin" Value="4"/>
</Style> </Style>
@ -59,6 +59,8 @@
</Style.Triggers> </Style.Triggers>
</Style> </Style>
<Style x:Key="Routingkey" TargetType="{x:Type TextBlock}"> <Style x:Key="TypeSelection" TargetType="{x:Type FrameworkElement}">
<Setter Property="Margin" Value="0 0 8 0" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style> </Style>
</ResourceDictionary> </ResourceDictionary>

View File

@ -0,0 +1,41 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace PettingZoo.UI
{
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void RaiseOtherPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, IEqualityComparer<T>? comparer = null, [CallerMemberName] string? propertyName = null,
params string[]? otherPropertiesChanged)
{
if ((comparer ?? EqualityComparer<T>.Default).Equals(field, value))
return false;
field = value;
RaisePropertyChanged(propertyName);
if (otherPropertiesChanged == null)
return true;
foreach (var otherProperty in otherPropertiesChanged)
RaisePropertyChanged(otherProperty);
return true;
}
}
}

View File

@ -0,0 +1,117 @@
using System;
using System.Windows.Input;
// TODO validate input
namespace PettingZoo.UI.Connection
{
public class ConnectionViewModel : BaseViewModel
{
private string host;
private string virtualHost;
private int port;
private string username;
private string password;
private bool subscribe;
private string exchange;
private string routingKey;
public string Host
{
get => host;
set => SetField(ref host, value);
}
public string VirtualHost
{
get => virtualHost;
set => SetField(ref virtualHost, value);
}
public int Port
{
get => port;
set => SetField(ref port, value);
}
public string Username
{
get => username;
set => SetField(ref username, value);
}
public string Password
{
get => password;
set => SetField(ref password, value);
}
public bool Subscribe
{
get => subscribe;
set => SetField(ref subscribe, value);
}
public string Exchange
{
get => exchange;
set => SetField(ref exchange, value);
}
public string RoutingKey
{
get => routingKey;
set => SetField(ref routingKey, value);
}
public ICommand OkCommand { get; }
public event EventHandler? OkClick;
public ConnectionViewModel(ConnectionDialogParams model)
{
OkCommand = new DelegateCommand(OkExecute, OkCanExecute);
host = model.Host;
virtualHost = model.VirtualHost;
port = model.Port;
username = model.Username;
password = model.Password;
subscribe = model.Subscribe;
exchange = model.Exchange;
routingKey = model.RoutingKey;
}
public ConnectionDialogParams ToModel()
{
return new(Host, VirtualHost, Port, Username, Password, Subscribe, Exchange, RoutingKey);
}
private void OkExecute()
{
OkClick?.Invoke(this, EventArgs.Empty);
}
private static bool OkCanExecute()
{
return true;
}
}
public class DesignTimeConnectionViewModel : ConnectionViewModel
{
public DesignTimeConnectionViewModel() : base(ConnectionDialogParams.Default)
{
}
}
}

View File

@ -1,27 +1,26 @@
<Window x:Class="PettingZoo.View.ConnectionWindow" <Window x:Class="PettingZoo.UI.Connection.ConnectionWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:res="clr-namespace:PettingZoo.Properties"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModel="clr-namespace:PettingZoo.ViewModel" xmlns:ui="clr-namespace:PettingZoo.UI"
xmlns:infrastructure="clr-namespace:PettingZoo.Infrastructure" xmlns:connection="clr-namespace:PettingZoo.UI.Connection"
mc:Ignorable="d" mc:Ignorable="d"
d:DataContext="{d:DesignInstance viewModel:ConnectionViewModel}" d:DataContext="{d:DesignInstance connection:DesignTimeConnectionViewModel, IsDesignTimeCreatable = True}"
Width="500" Width="500"
SizeToContent="Height" SizeToContent="Height"
ResizeMode="NoResize" ResizeMode="NoResize"
WindowStyle="ToolWindow" WindowStyle="ToolWindow"
Style="{StaticResource WindowStyle}" Style="{StaticResource WindowStyle}"
Title="{x:Static res:Resources.ConnectionWindowTitle}" Title="{x:Static connection:ConnectionWindowStrings.WindowTitle}"
FocusManager.FocusedElement="{Binding ElementName=HostTextBox}"> FocusManager.FocusedElement="{Binding ElementName=HostTextBox}">
<DockPanel Margin="8"> <DockPanel Margin="8">
<UniformGrid DockPanel.Dock="Bottom" HorizontalAlignment="Right" Rows="1" Columns="2" Style="{StaticResource FooterPanel}"> <UniformGrid DockPanel.Dock="Bottom" HorizontalAlignment="Right" Rows="1" Columns="2" Style="{StaticResource FooterPanel}">
<Button IsDefault="True" Content="{x:Static res:Resources.ButtonOK}" Style="{StaticResource FooterButton}" Command="{Binding OkCommand}"/> <Button IsDefault="True" Content="{x:Static connection:ConnectionWindowStrings.ButtonOK}" Style="{StaticResource FooterButton}" Command="{Binding OkCommand}"/>
<Button IsCancel="True" Content="{x:Static res:Resources.ButtonCancel}" Style="{StaticResource FooterButton}"/> <Button IsCancel="True" Content="{x:Static connection:ConnectionWindowStrings.ButtonCancel}" Style="{StaticResource FooterButton}"/>
</UniformGrid> </UniformGrid>
<infrastructure:GridLayout Style="{StaticResource Form}"> <ui:GridLayout Style="{StaticResource Form}">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
@ -35,28 +34,31 @@
<RowDefinition Height="8"/> <RowDefinition Height="8"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0" Content="{x:Static res:Resources.ConnectionHost}"/> <Label Grid.Column="0" Grid.Row="0" Content="{x:Static connection:ConnectionWindowStrings.LabelHost}"/>
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Host}" Name="HostTextBox" /> <TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Host}" Name="HostTextBox" />
<Label Grid.Column="0" Grid.Row="1" Content="{x:Static res:Resources.ConnectionPort}"/> <Label Grid.Column="0" Grid.Row="1" Content="{x:Static connection:ConnectionWindowStrings.LabelPort}"/>
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Port}" Width="100" HorizontalAlignment="Left" PreviewTextInput="NumericPreviewTextInput" /> <TextBox Grid.Column="1" Grid.Row="1" Text="{Binding Port}" Width="100" HorizontalAlignment="Left" PreviewTextInput="NumericPreviewTextInput" />
<Label Grid.Column="0" Grid.Row="2" Content="{x:Static res:Resources.ConnectionVirtualHost}"/> <Label Grid.Column="0" Grid.Row="2" Content="{x:Static connection:ConnectionWindowStrings.LabelVirtualHost}"/>
<TextBox Grid.Column="1" Grid.Row="2" Text="{Binding VirtualHost}"/> <TextBox Grid.Column="1" Grid.Row="2" Text="{Binding VirtualHost}"/>
<Label Grid.Column="0" Grid.Row="3" Content="{x:Static res:Resources.ConnectionUsername}"/> <Label Grid.Column="0" Grid.Row="3" Content="{x:Static connection:ConnectionWindowStrings.LabelUsername}"/>
<TextBox Grid.Column="1" Grid.Row="3" Text="{Binding Username}"/> <TextBox Grid.Column="1" Grid.Row="3" Text="{Binding Username}"/>
<Label Grid.Column="0" Grid.Row="4" Content="{x:Static res:Resources.ConnectionPassword}"/> <Label Grid.Column="0" Grid.Row="4" Content="{x:Static connection:ConnectionWindowStrings.LabelPassword}"/>
<PasswordBox Grid.Column="1" Grid.Row="4" infrastructure:PasswordBoxAssistant.BindPassword="true" infrastructure:PasswordBoxAssistant.BoundPassword="{Binding Path=Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <PasswordBox Grid.Column="1" Grid.Row="4" ui:PasswordBoxAssistant.BindPassword="true" ui:PasswordBoxAssistant.BoundPassword="{Binding Path=Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Label Grid.Column="0" Grid.Row="6" Content="{x:Static res:Resources.ConnectionExchange}"/> <CheckBox Grid.Column="1" Grid.Row="6" Content="{x:Static connection:ConnectionWindowStrings.LabelSubscribe}" IsChecked="{Binding Subscribe}"/>
<TextBox Grid.Column="1" Grid.Row="6" Text="{Binding Exchange}"/>
<Label Grid.Column="0" Grid.Row="7" Content="{x:Static res:Resources.ConnectionRoutingKey}"/> <Label Grid.Column="0" Grid.Row="7" Content="{x:Static connection:ConnectionWindowStrings.LabelExchange}"/>
<TextBox Grid.Column="1" Grid.Row="7" Text="{Binding RoutingKey}"/> <TextBox Grid.Column="1" Grid.Row="7" Text="{Binding Exchange}"/>
</infrastructure:GridLayout>
<Label Grid.Column="0" Grid.Row="8" Content="{x:Static connection:ConnectionWindowStrings.LabelRoutingKey}"/>
<TextBox Grid.Column="1" Grid.Row="8" Text="{Binding RoutingKey}"/>
</ui:GridLayout>
</DockPanel> </DockPanel>
</Window> </Window>

View File

@ -0,0 +1,45 @@
using System.Windows;
using System.Windows.Input;
namespace PettingZoo.UI.Connection
{
public class WindowConnectionDialog : IConnectionDialog
{
public ConnectionDialogParams? Show(ConnectionDialogParams? defaultParams = null)
{
var viewModel = new ConnectionViewModel(defaultParams ?? ConnectionDialogParams.Default);
var window = new ConnectionWindow(viewModel)
{
Owner = Application.Current.MainWindow
};
viewModel.OkClick += (_, _) =>
{
window.DialogResult = true;
};
return window.ShowDialog().GetValueOrDefault()
? viewModel.ToModel()
: null;
}
}
public partial class ConnectionWindow
{
public ConnectionWindow(ConnectionViewModel viewModel)
{
WindowStartupLocation = WindowStartupLocation.CenterOwner;
InitializeComponent();
DataContext = viewModel;
}
private void NumericPreviewTextInput(object sender, TextCompositionEventArgs args)
{
if (!char.IsDigit(args.Text, args.Text.Length - 1))
args.Handled = true;
}
}
}

View File

@ -0,0 +1,162 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PettingZoo.UI.Connection {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class ConnectionWindowStrings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal ConnectionWindowStrings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Connection.ConnectionWindowStrings", typeof(ConnectionWindowStrings).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Cancel.
/// </summary>
public static string ButtonCancel {
get {
return ResourceManager.GetString("ButtonCancel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to OK.
/// </summary>
public static string ButtonOK {
get {
return ResourceManager.GetString("ButtonOK", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Exchange:.
/// </summary>
public static string LabelExchange {
get {
return ResourceManager.GetString("LabelExchange", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Host:.
/// </summary>
public static string LabelHost {
get {
return ResourceManager.GetString("LabelHost", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Password:.
/// </summary>
public static string LabelPassword {
get {
return ResourceManager.GetString("LabelPassword", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Port:.
/// </summary>
public static string LabelPort {
get {
return ResourceManager.GetString("LabelPort", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Routing key:.
/// </summary>
public static string LabelRoutingKey {
get {
return ResourceManager.GetString("LabelRoutingKey", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Subscribe.
/// </summary>
public static string LabelSubscribe {
get {
return ResourceManager.GetString("LabelSubscribe", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Username:.
/// </summary>
public static string LabelUsername {
get {
return ResourceManager.GetString("LabelUsername", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Virtual host:.
/// </summary>
public static string LabelVirtualHost {
get {
return ResourceManager.GetString("LabelVirtualHost", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connection parameters.
/// </summary>
public static string WindowTitle {
get {
return ResourceManager.GetString("WindowTitle", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ButtonCancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="ButtonOK" xml:space="preserve">
<value>OK</value>
</data>
<data name="LabelExchange" xml:space="preserve">
<value>Exchange:</value>
</data>
<data name="LabelHost" xml:space="preserve">
<value>Host:</value>
</data>
<data name="LabelPassword" xml:space="preserve">
<value>Password:</value>
</data>
<data name="LabelPort" xml:space="preserve">
<value>Port:</value>
</data>
<data name="LabelRoutingKey" xml:space="preserve">
<value>Routing key:</value>
</data>
<data name="LabelSubscribe" xml:space="preserve">
<value>Subscribe</value>
</data>
<data name="LabelUsername" xml:space="preserve">
<value>Username:</value>
</data>
<data name="LabelVirtualHost" xml:space="preserve">
<value>Virtual host:</value>
</data>
<data name="WindowTitle" xml:space="preserve">
<value>Connection parameters</value>
</data>
</root>

View File

@ -0,0 +1,43 @@
using System;
namespace PettingZoo.UI.Connection
{
public interface IConnectionDialog
{
ConnectionDialogParams? Show(ConnectionDialogParams? defaultParams = null);
}
public class ConnectionDialogParams
{
public string Host { get; }
public string VirtualHost { get; }
public int Port { get; }
public string Username { get; }
public string Password { get; }
public bool Subscribe { get; }
public string Exchange { get; }
public string RoutingKey { get; }
public static ConnectionDialogParams Default { get; } = new("localhost", "/", 5672, "guest", "guest", false, "", "#");
public ConnectionDialogParams(string host, string virtualHost, int port, string username, string password, bool subscribe, string exchange, string routingKey)
{
if (subscribe && (string.IsNullOrEmpty(exchange) || string.IsNullOrEmpty(routingKey)))
throw new ArgumentException(@"Exchange and RoutingKey must be provided when Subscribe is true", nameof(subscribe));
Host = host;
VirtualHost = virtualHost;
Port = port;
Username = username;
Password = password;
Subscribe = subscribe;
Exchange = exchange;
RoutingKey = routingKey;
}
}
}

View File

@ -0,0 +1,79 @@
using System;
using System.Windows.Input;
namespace PettingZoo.UI
{
public class DelegateCommand<T> : ICommand where T : class?
{
private readonly Func<T?, bool>? canExecute;
private readonly Action<T?> execute;
public event EventHandler? CanExecuteChanged;
public DelegateCommand(Action<T?> execute) : this(execute, null)
{
}
public DelegateCommand(Action<T?> execute, Func<T?, bool>? canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return canExecute == null || canExecute((T?)parameter);
}
public void Execute(object? parameter)
{
execute((T?)parameter);
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
public class DelegateCommand : ICommand
{
private readonly Func<bool>? canExecute;
private readonly Action execute;
public event EventHandler? CanExecuteChanged;
public DelegateCommand(Action execute) : this(execute, null) { }
public DelegateCommand(Action execute, Func<bool>? canExecute)
{
this.execute = execute;
this.canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return canExecute == null || canExecute();
}
public void Execute(object? parameter)
{
execute();
}
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}

View File

@ -1,7 +1,7 @@
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace PettingZoo.Infrastructure namespace PettingZoo.UI
{ {
// Source: http://daniel-albuschat.blogspot.nl/2011/07/gridlayout-for-wpf-escape-margin-hell.html // Source: http://daniel-albuschat.blogspot.nl/2011/07/gridlayout-for-wpf-escape-margin-hell.html
@ -32,7 +32,7 @@ namespace PettingZoo.Infrastructure
public Thickness ChildMargin public Thickness ChildMargin
{ {
get { return (Thickness) GetValue(ChildMarginProperty); } get => (Thickness) GetValue(ChildMarginProperty);
set set
{ {
SetValue(ChildMarginProperty, value); SetValue(ChildMarginProperty, value);
@ -45,12 +45,13 @@ namespace PettingZoo.Infrastructure
public void UpdateChildMargins() public void UpdateChildMargins()
{ {
int maxColumn = 0; var maxColumn = 0;
int maxRow = 0; var maxRow = 0;
foreach (UIElement element in InternalChildren) foreach (UIElement element in InternalChildren)
{ {
int row = GetRow(element); var row = GetRow(element);
int column = GetColumn(element); var column = GetColumn(element);
if (row > maxRow) if (row > maxRow)
maxRow = row; maxRow = row;
if (column > maxColumn) if (column > maxColumn)
@ -58,15 +59,15 @@ namespace PettingZoo.Infrastructure
} }
foreach (UIElement element in InternalChildren) foreach (UIElement element in InternalChildren)
{ {
var fe = element as FrameworkElement; if (element is not FrameworkElement fe)
if (null != fe) continue;
{
int row = GetRow(fe); var row = GetRow(fe);
int column = GetColumn(fe); var column = GetColumn(fe);
double factorLeft = 0.5; var factorLeft = 0.5;
double factorTop = 0.5; var factorTop = 0.5;
double factorRight = 0.5; var factorRight = 0.5;
double factorBottom = 0.5; var factorBottom = 0.5;
// Top row - no top margin // Top row - no top margin
if (row == 0) if (row == 0)
factorTop = 0; factorTop = 0;
@ -85,7 +86,6 @@ namespace PettingZoo.Infrastructure
ChildMargin.Bottom*factorBottom); ChildMargin.Bottom*factorBottom);
} }
} }
}
// We change all children's margins in MeasureOverride, since this is called right before // We change all children's margins in MeasureOverride, since this is called right before
// the layouting takes place. I was first skeptical to do this here, because I thought changing // the layouting takes place. I was first skeptical to do this here, because I thought changing

View File

@ -2,11 +2,11 @@
using System.Collections; using System.Collections;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Windows; using System.Windows;
using System.Windows.Data;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media; using System.Windows.Media;
namespace PettingZoo.Infrastructure namespace PettingZoo.UI
{ {
// Source: https://social.msdn.microsoft.com/Forums/vstudio/en-US/0f524459-b14e-4f9a-8264-267953418a2d/trivial-listboxlistview-autoscroll?forum=wpf // Source: https://social.msdn.microsoft.com/Forums/vstudio/en-US/0f524459-b14e-4f9a-8264-267953418a2d/trivial-listboxlistview-autoscroll?forum=wpf
// //
@ -29,7 +29,7 @@ namespace PettingZoo.Infrastructure
public static void SetAutoScroll(System.Windows.Controls.ListBox instance, bool value) public static void SetAutoScroll(System.Windows.Controls.ListBox instance, bool value)
{ {
var oldHandler = (AutoScrollHandler) instance.GetValue(AutoScrollHandlerProperty); var oldHandler = (AutoScrollHandler?)instance.GetValue(AutoScrollHandlerProperty);
if (oldHandler != null) if (oldHandler != null)
{ {
oldHandler.Dispose(); oldHandler.Dispose();
@ -51,7 +51,7 @@ namespace PettingZoo.Infrastructure
ItemsSourcePropertyChanged)); ItemsSourcePropertyChanged));
private readonly System.Windows.Controls.ListBox target; private readonly System.Windows.Controls.ListBox target;
private ScrollViewer scrollViewer; private ScrollViewer? scrollViewer;
public AutoScrollHandler(System.Windows.Controls.ListBox target) public AutoScrollHandler(System.Windows.Controls.ListBox target)
{ {
@ -74,8 +74,8 @@ namespace PettingZoo.Infrastructure
public IEnumerable ItemsSource public IEnumerable ItemsSource
{ {
get { return (IEnumerable) GetValue(ItemsSourceProperty); } get => (IEnumerable) GetValue(ItemsSourceProperty);
set { SetValue(ItemsSourceProperty, value); } set => SetValue(ItemsSourceProperty, value);
} }
@ -87,16 +87,14 @@ namespace PettingZoo.Infrastructure
private void ItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) private void ItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{ {
var collection = oldValue as INotifyCollectionChanged; if (oldValue is INotifyCollectionChanged oldCollection)
if (collection != null) oldCollection.CollectionChanged -= Collection_CollectionChanged;
collection.CollectionChanged -= Collection_CollectionChanged;
collection = newValue as INotifyCollectionChanged; if (newValue is INotifyCollectionChanged newCollection)
if (collection != null) newCollection.CollectionChanged += Collection_CollectionChanged;
collection.CollectionChanged += Collection_CollectionChanged;
} }
private void Collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) private void Collection_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{ {
if (e.Action != NotifyCollectionChangedAction.Add || e.NewItems == null || e.NewItems.Count < 1) if (e.Action != NotifyCollectionChangedAction.Add || e.NewItems == null || e.NewItems.Count < 1)
return; return;
@ -109,24 +107,31 @@ namespace PettingZoo.Infrastructure
return; return;
} }
if (Math.Abs(scrollViewer.VerticalOffset - scrollViewer.ScrollableHeight) < 1) if (e.NewItems.Count == 0)
target.ScrollIntoView(e.NewItems[e.NewItems.Count - 1]); return;
// If not already at the bottom, keep the position stable
if (Math.Abs(scrollViewer.VerticalOffset - scrollViewer.ScrollableHeight) > 0)
return;
var item = e.NewItems[^1];
if (item != null)
target.ScrollIntoView(item);
} }
private static ScrollViewer FindScrollViewer(DependencyObject parent) private static ScrollViewer? FindScrollViewer(DependencyObject parent)
{ {
var childCount = VisualTreeHelper.GetChildrenCount(parent); var childCount = VisualTreeHelper.GetChildrenCount(parent);
for (var childIndex = 0; childIndex < childCount; childIndex++) for (var childIndex = 0; childIndex < childCount; childIndex++)
{ {
var child = VisualTreeHelper.GetChild(parent, childIndex); var child = VisualTreeHelper.GetChild(parent, childIndex);
var scrollViewer = (child as ScrollViewer); if (child is ScrollViewer scrollViewer)
if (scrollViewer != null)
return scrollViewer; return scrollViewer;
scrollViewer = FindScrollViewer(child); var childScrollViewer = FindScrollViewer(child);
if (scrollViewer != null) if (childScrollViewer != null)
return scrollViewer; return childScrollViewer;
} }
return null; return null;

View File

@ -0,0 +1,97 @@
<Window x:Class="PettingZoo.UI.Main.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:main="clr-namespace:PettingZoo.UI.Main"
xmlns:tab="clr-namespace:PettingZoo.UI.Tab"
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance main:DesignTimeMainWindowViewModel, IsDesignTimeCreatable=True}"
Width="800"
Height="600"
ResizeMode="CanResizeWithGrip"
Style="{StaticResource WindowStyle}"
Title="{x:Static main:MainWindowStrings.WindowTitle}"
Loaded="MainWindow_OnLoaded"
Closed="MainWindow_OnClosed">
<Window.InputBindings>
<KeyBinding Modifiers="Control" Key="W" Command="{Binding CloseTabCommand}" />
</Window.InputBindings>
<DockPanel>
<ToolBar DockPanel.Dock="Top" ToolBarTray.IsLocked="True">
<Button Command="{Binding ConnectCommand}">
<StackPanel Orientation="Horizontal">
<Image Source="{svgc:SvgImage Source=/Images/Connect.svg, AppName=PettingZoo}" Width="16" Height="16" Style="{StaticResource ToolbarIcon}"/>
<TextBlock Margin="3,0,0,0" Text="{x:Static main:MainWindowStrings.CommandConnect}" />
</StackPanel>
</Button>
<Button Command="{Binding DisconnectCommand}">
<StackPanel Orientation="Horizontal">
<Image Source="{svgc:SvgImage Source=/Images/Disconnect.svg, AppName=PettingZoo}" Width="16" Height="16" Style="{StaticResource ToolbarIcon}"/>
<TextBlock Margin="3,0,0,0" Text="{x:Static main:MainWindowStrings.CommandDisconnect}" />
</StackPanel>
</Button>
<Separator Style="{StaticResource {x:Static ToolBar.SeparatorStyleKey}}" />
<Button Command="{Binding SubscribeCommand}">
<StackPanel Orientation="Horizontal">
<Image Source="{svgc:SvgImage Source=/Images/Subscribe.svg, AppName=PettingZoo}" Width="16" Height="16" Style="{StaticResource ToolbarIcon}"/>
<TextBlock Margin="3,0,0,0" Text="{x:Static main:MainWindowStrings.CommandSubscribe}" />
</StackPanel>
</Button>
<Button Command="{Binding PublishCommand}">
<StackPanel Orientation="Horizontal">
<Image Source="{svgc:SvgImage Source=/Images/Publish.svg, AppName=PettingZoo}" Width="16" Height="16" Style="{StaticResource ToolbarIcon}"/>
<TextBlock Margin="3,0,0,0" Text="{x:Static main:MainWindowStrings.CommandPublish}" />
</StackPanel>
</Button>
<Separator Style="{StaticResource {x:Static ToolBar.SeparatorStyleKey}}" Visibility="{Binding ToolbarCommandsSeparatorVisibility}" />
<ItemsControl ItemsSource="{Binding ToolbarCommands}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding Command}" Style="{DynamicResource {x:Static ToolBar.ButtonStyleKey}}">
<StackPanel Orientation="Horizontal">
<Image Source="{Binding Icon}" Width="16" Height="16" Style="{StaticResource ToolbarIcon}"/>
<TextBlock Margin="3,0,0,0" Text="{Binding Caption}" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ToolBar>
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem>
<TextBlock Text="{Binding ConnectionStatus}"/>
</StatusBarItem>
</StatusBar>
<TabControl
Name="SubscriberTabs"
ItemsSource="{Binding Tabs}"
SelectedValue="{Binding ActiveTab}">
<TabControl.ItemContainerStyle>
<Style TargetType="{x:Type TabItem}">
<Setter Property="Header" Value="{Binding Title}" />
<Setter Property="ContextMenu">
<Setter.Value>
<ContextMenu>
<MenuItem Header="{x:Static main:MainWindowStrings.ContextMenuCloseTab}" Command="{Binding CloseTabCommand}" InputGestureText="Ctrl+W" />
</ContextMenu>
</Setter.Value>
</Setter>
</Style>
</TabControl.ItemContainerStyle>
<TabControl.ContentTemplate>
<DataTemplate DataType="{x:Type tab:ITab}">
<ContentControl Content="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>

View File

@ -0,0 +1,50 @@
using System;
using System.Windows;
using PettingZoo.Core.Connection;
using PettingZoo.UI.Connection;
using PettingZoo.UI.Subscribe;
using PettingZoo.UI.Tab;
namespace PettingZoo.UI.Main
{
// TODO support undocking tabs (and redocking afterwards)
// TODO allow tab reordering
#pragma warning disable CA1001 // MainWindow can't be IDisposable, handled instead in OnDispatcherShutDownStarted
public partial class MainWindow
{
private readonly MainWindowViewModel viewModel;
public MainWindow(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog, ISubscribeDialog subscribeDialog, ITabFactory tabFactory)
{
WindowStartupLocation = WindowStartupLocation.CenterScreen;
InitializeComponent();
viewModel = new MainWindowViewModel(connectionFactory, connectionDialog, subscribeDialog, tabFactory);
DataContext = viewModel;
Dispatcher.ShutdownStarted += OnDispatcherShutDownStarted;
}
private async void OnDispatcherShutDownStarted(object? sender, EventArgs e)
{
if (DataContext is IAsyncDisposable disposable)
await disposable.DisposeAsync();
}
private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
{
viewModel.ConnectCommand.Execute(null);
}
private void MainWindow_OnClosed(object? sender, EventArgs e)
{
var _ = Application.Current.Windows;
}
}
#pragma warning restore CA1001
}

View File

@ -8,7 +8,10 @@
// </auto-generated> // </auto-generated>
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
namespace PettingZoo.Properties { namespace PettingZoo.UI.Main {
using System;
/// <summary> /// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc. /// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary> /// </summary>
@ -16,17 +19,17 @@ namespace PettingZoo.Properties {
// class via a tool like ResGen or Visual Studio. // class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen // To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project. // with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class Resources { public class MainWindowStrings {
private static global::System.Resources.ResourceManager resourceMan; private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture; private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() { internal MainWindowStrings() {
} }
/// <summary> /// <summary>
@ -36,7 +39,7 @@ namespace PettingZoo.Properties {
public static global::System.Resources.ResourceManager ResourceManager { public static global::System.Resources.ResourceManager ResourceManager {
get { get {
if (object.ReferenceEquals(resourceMan, null)) { if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.Properties.Resources", typeof(Resources).Assembly); global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Main.MainWindowStrings", typeof(MainWindowStrings).Assembly);
resourceMan = temp; resourceMan = temp;
} }
return resourceMan; return resourceMan;
@ -58,92 +61,47 @@ namespace PettingZoo.Properties {
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Cancel. /// Looks up a localized string similar to Connect.
/// </summary> /// </summary>
public static string ButtonCancel { public static string CommandConnect {
get { get {
return ResourceManager.GetString("ButtonCancel", resourceCulture); return ResourceManager.GetString("CommandConnect", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to OK. /// Looks up a localized string similar to Disconnect.
/// </summary> /// </summary>
public static string ButtonOK { public static string CommandDisconnect {
get { get {
return ResourceManager.GetString("ButtonOK", resourceCulture); return ResourceManager.GetString("CommandDisconnect", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Exchange:. /// Looks up a localized string similar to New Publisher.
/// </summary> /// </summary>
public static string ConnectionExchange { public static string CommandPublish {
get { get {
return ResourceManager.GetString("ConnectionExchange", resourceCulture); return ResourceManager.GetString("CommandPublish", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Host:. /// Looks up a localized string similar to New Subscriber....
/// </summary> /// </summary>
public static string ConnectionHost { public static string CommandSubscribe {
get { get {
return ResourceManager.GetString("ConnectionHost", resourceCulture); return ResourceManager.GetString("CommandSubscribe", resourceCulture);
} }
} }
/// <summary> /// <summary>
/// Looks up a localized string similar to Password:. /// Looks up a localized string similar to Close tab.
/// </summary> /// </summary>
public static string ConnectionPassword { public static string ContextMenuCloseTab {
get { get {
return ResourceManager.GetString("ConnectionPassword", resourceCulture); return ResourceManager.GetString("ContextMenuCloseTab", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Port:.
/// </summary>
public static string ConnectionPort {
get {
return ResourceManager.GetString("ConnectionPort", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Routing key:.
/// </summary>
public static string ConnectionRoutingKey {
get {
return ResourceManager.GetString("ConnectionRoutingKey", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Username:.
/// </summary>
public static string ConnectionUsername {
get {
return ResourceManager.GetString("ConnectionUsername", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Virtual host:.
/// </summary>
public static string ConnectionVirtualHost {
get {
return ResourceManager.GetString("ConnectionVirtualHost", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Connection parameters.
/// </summary>
public static string ConnectionWindowTitle {
get {
return ResourceManager.GetString("ConnectionWindowTitle", resourceCulture);
} }
} }
@ -165,51 +123,6 @@ namespace PettingZoo.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Petting Zoo - a RabbitMQ live message viewer.
/// </summary>
public static string MainWindowTitle {
get {
return ResourceManager.GetString("MainWindowTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Body.
/// </summary>
public static string PanelTitleBody {
get {
return ResourceManager.GetString("PanelTitleBody", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Properties.
/// </summary>
public static string PanelTitleProperties {
get {
return ResourceManager.GetString("PanelTitleProperties", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name.
/// </summary>
public static string PropertyName {
get {
return ResourceManager.GetString("PropertyName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Value.
/// </summary>
public static string PropertyValue {
get {
return ResourceManager.GetString("PropertyValue", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Connected. /// Looks up a localized string similar to Connected.
/// </summary> /// </summary>
@ -245,5 +158,14 @@ namespace PettingZoo.Properties {
return ResourceManager.GetString("StatusError", resourceCulture); return ResourceManager.GetString("StatusError", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Petting Zoo - a RabbitMQ live message viewer.
/// </summary>
public static string WindowTitle {
get {
return ResourceManager.GetString("WindowTitle", resourceCulture);
}
}
} }
} }

View File

@ -117,35 +117,20 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="ButtonCancel" xml:space="preserve"> <data name="CommandConnect" xml:space="preserve">
<value>Cancel</value> <value>Connect</value>
</data> </data>
<data name="ButtonOK" xml:space="preserve"> <data name="CommandDisconnect" xml:space="preserve">
<value>OK</value> <value>Disconnect</value>
</data> </data>
<data name="ConnectionExchange" xml:space="preserve"> <data name="CommandPublish" xml:space="preserve">
<value>Exchange:</value> <value>New Publisher</value>
</data> </data>
<data name="ConnectionHost" xml:space="preserve"> <data name="CommandSubscribe" xml:space="preserve">
<value>Host:</value> <value>New Subscriber...</value>
</data> </data>
<data name="ConnectionPassword" xml:space="preserve"> <data name="ContextMenuCloseTab" xml:space="preserve">
<value>Password:</value> <value>Close tab</value>
</data>
<data name="ConnectionPort" xml:space="preserve">
<value>Port:</value>
</data>
<data name="ConnectionRoutingKey" xml:space="preserve">
<value>Routing key:</value>
</data>
<data name="ConnectionUsername" xml:space="preserve">
<value>Username:</value>
</data>
<data name="ConnectionVirtualHost" xml:space="preserve">
<value>Virtual host:</value>
</data>
<data name="ConnectionWindowTitle" xml:space="preserve">
<value>Connection parameters</value>
</data> </data>
<data name="DeliveryModeNonPersistent" xml:space="preserve"> <data name="DeliveryModeNonPersistent" xml:space="preserve">
<value>Non-persistent</value> <value>Non-persistent</value>
@ -153,21 +138,6 @@
<data name="DeliveryModePersistent" xml:space="preserve"> <data name="DeliveryModePersistent" xml:space="preserve">
<value>Persistent</value> <value>Persistent</value>
</data> </data>
<data name="MainWindowTitle" xml:space="preserve">
<value>Petting Zoo - a RabbitMQ live message viewer</value>
</data>
<data name="PanelTitleBody" xml:space="preserve">
<value>Body</value>
</data>
<data name="PanelTitleProperties" xml:space="preserve">
<value>Properties</value>
</data>
<data name="PropertyName" xml:space="preserve">
<value>Name</value>
</data>
<data name="PropertyValue" xml:space="preserve">
<value>Value</value>
</data>
<data name="StatusConnected" xml:space="preserve"> <data name="StatusConnected" xml:space="preserve">
<value>Connected</value> <value>Connected</value>
</data> </data>
@ -180,4 +150,7 @@
<data name="StatusError" xml:space="preserve"> <data name="StatusError" xml:space="preserve">
<value>Error: {0}</value> <value>Error: {0}</value>
</data> </data>
<data name="WindowTitle" xml:space="preserve">
<value>Petting Zoo - a RabbitMQ live message viewer</value>
</data>
</root> </root>

View File

@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using PettingZoo.Core.Connection;
using PettingZoo.UI.Connection;
using PettingZoo.UI.Subscribe;
using PettingZoo.UI.Tab;
namespace PettingZoo.UI.Main
{
public class MainWindowViewModel : BaseViewModel, IAsyncDisposable
{
private readonly IConnectionFactory connectionFactory;
private readonly IConnectionDialog connectionDialog;
private readonly ISubscribeDialog subscribeDialog;
private readonly ITabFactory tabFactory;
private ConnectionDialogParams? connectionDialogParams;
private SubscribeDialogParams? subscribeDialogParams;
private IConnection? connection;
private string connectionStatus;
private ITab? activeTab;
private readonly DelegateCommand connectCommand;
private readonly DelegateCommand disconnectCommand;
private readonly DelegateCommand publishCommand;
private readonly DelegateCommand subscribeCommand;
private readonly DelegateCommand closeTabCommand;
public string ConnectionStatus
{
get => connectionStatus;
private set => SetField(ref connectionStatus, value);
}
public ObservableCollection<ITab> Tabs { get; }
public ITab? ActiveTab
{
get => activeTab;
set => SetField(ref activeTab, value, otherPropertiesChanged: new []
{
nameof(ToolbarCommands),
nameof(ToolbarCommandsSeparatorVisibility)
});
}
public ICommand ConnectCommand => connectCommand;
public ICommand DisconnectCommand => disconnectCommand;
public ICommand PublishCommand => publishCommand;
public ICommand SubscribeCommand => subscribeCommand;
public ICommand CloseTabCommand => closeTabCommand;
public IEnumerable<TabToolbarCommand> ToolbarCommands => ActiveTab is ITabToolbarCommands tabToolbarCommands
? tabToolbarCommands.ToolbarCommands
: Enumerable.Empty<TabToolbarCommand>();
public Visibility ToolbarCommandsSeparatorVisibility =>
ToolbarCommands.Any() ? Visibility.Visible : Visibility.Collapsed;
public MainWindowViewModel(IConnectionFactory connectionFactory, IConnectionDialog connectionDialog,
ISubscribeDialog subscribeDialog, ITabFactory tabFactory)
{
this.connectionFactory = connectionFactory;
this.connectionDialog = connectionDialog;
this.subscribeDialog = subscribeDialog;
this.tabFactory = tabFactory;
connectionStatus = GetConnectionStatus(null);
Tabs = new ObservableCollection<ITab>();
connectCommand = new DelegateCommand(ConnectExecute);
disconnectCommand = new DelegateCommand(DisconnectExecute, IsConnectedCanExecute);
publishCommand = new DelegateCommand(PublishExecute, IsConnectedCanExecute);
subscribeCommand = new DelegateCommand(SubscribeExecute, IsConnectedCanExecute);
closeTabCommand = new DelegateCommand(CloseTabExecute, CloseTabCanExecute);
}
public async ValueTask DisposeAsync()
{
if (connection != null)
await connection.DisposeAsync();
}
private async void ConnectExecute()
{
//var newParams = connectionDialog.Show(connectionDialogParams);
var newParams = new ConnectionDialogParams("localhost", "/", 5672, "guest", "guest", true, "lef", "#");
if (newParams == null)
return;
if (connection != null)
await connection.DisposeAsync();
connectionDialogParams = newParams;
connection = connectionFactory.CreateConnection(new ConnectionParams(
connectionDialogParams.Host, connectionDialogParams.VirtualHost, connectionDialogParams.Port,
connectionDialogParams.Username, connectionDialogParams.Password));
connection.StatusChanged += ConnectionStatusChanged;
if (connectionDialogParams.Subscribe)
{
var subscriber = connection.Subscribe(connectionDialogParams.Exchange, connectionDialogParams.RoutingKey);
AddTab(tabFactory.CreateSubscriberTab(CloseTabCommand, subscriber));
}
ConnectionChanged();
}
private async void DisconnectExecute()
{
Tabs.Clear();
if (connection != null)
{
await connection.DisposeAsync();
connection = null;
}
connectionDialogParams = null;
ConnectionStatus = GetConnectionStatus(null);
ConnectionChanged();
}
private void SubscribeExecute()
{
if (connection == null)
return;
var newParams = subscribeDialog.Show(subscribeDialogParams);
if (newParams == null)
return;
subscribeDialogParams = newParams;
var subscriber = connection.Subscribe(subscribeDialogParams.Exchange, subscribeDialogParams.RoutingKey);
AddTab(tabFactory.CreateSubscriberTab(CloseTabCommand, subscriber));
}
private void PublishExecute()
{
if (connection == null)
return;
AddTab(tabFactory.CreatePublisherTab(CloseTabCommand, connection));
}
private bool IsConnectedCanExecute()
{
return connection != null;
}
private void CloseTabExecute()
{
if (ActiveTab == null)
return;
var activeTabIndex = Tabs.IndexOf(ActiveTab);
if (activeTabIndex == -1)
return;
Tabs.RemoveAt(activeTabIndex);
if (activeTabIndex == Tabs.Count)
activeTabIndex--;
ActiveTab = activeTabIndex >= 0 ? Tabs[activeTabIndex] : null;
closeTabCommand.RaiseCanExecuteChanged();
}
private bool CloseTabCanExecute()
{
return ActiveTab != null;
}
private void AddTab(ITab tab)
{
Tabs.Add(tab);
ActiveTab = tab;
closeTabCommand.RaiseCanExecuteChanged();
}
private void ConnectionChanged()
{
disconnectCommand.RaiseCanExecuteChanged();
subscribeCommand.RaiseCanExecuteChanged();
publishCommand.RaiseCanExecuteChanged();
}
private void ConnectionStatusChanged(object? sender, StatusChangedEventArgs args)
{
ConnectionStatus = GetConnectionStatus(args);
}
private static string GetConnectionStatus(StatusChangedEventArgs? args)
{
return args?.Status switch
{
Core.Connection.ConnectionStatus.Connecting => string.Format(MainWindowStrings.StatusConnecting, args.Context),
Core.Connection.ConnectionStatus.Connected => string.Format(MainWindowStrings.StatusConnected, args.Context),
Core.Connection.ConnectionStatus.Error => string.Format(MainWindowStrings.StatusError, args.Context),
Core.Connection.ConnectionStatus.Disconnected => MainWindowStrings.StatusDisconnected,
_ => MainWindowStrings.StatusDisconnected
};
}
}
public class DesignTimeMainWindowViewModel : MainWindowViewModel
{
public DesignTimeMainWindowViewModel() : base(null!, null!, null!, null!)
{
}
}
}

View File

@ -1,7 +1,7 @@
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
namespace PettingZoo.Infrastructure namespace PettingZoo.UI
{ {
// Source: http://blog.functionalfun.net/2008/06/wpf-passwordbox-and-data-binding.html // Source: http://blog.functionalfun.net/2008/06/wpf-passwordbox-and-data-binding.html
public static class PasswordBoxAssistant public static class PasswordBoxAssistant
@ -35,9 +35,7 @@ namespace PettingZoo.Infrastructure
var newPassword = (string) e.NewValue; var newPassword = (string) e.NewValue;
if (!GetUpdatingPassword(box)) if (!GetUpdatingPassword(box))
{
box.Password = newPassword; box.Password = newPassword;
}
box.PasswordChanged += HandlePasswordChanged; box.PasswordChanged += HandlePasswordChanged;
} }
@ -46,27 +44,20 @@ namespace PettingZoo.Infrastructure
{ {
// when the BindPassword attached property is set on a PasswordBox, // when the BindPassword attached property is set on a PasswordBox,
// start listening to its PasswordChanged event // start listening to its PasswordChanged event
if (dp is not PasswordBox box)
var box = dp as PasswordBox;
if (box == null)
{ {
return; return;
} }
var wasBound = (bool) (e.OldValue); var wasBound = (bool)e.OldValue;
var needToBind = (bool) (e.NewValue); var needToBind = (bool)e.NewValue;
if (wasBound) if (wasBound)
{
box.PasswordChanged -= HandlePasswordChanged; box.PasswordChanged -= HandlePasswordChanged;
}
if (needToBind) if (needToBind)
{
box.PasswordChanged += HandlePasswordChanged; box.PasswordChanged += HandlePasswordChanged;
} }
}
private static void HandlePasswordChanged(object sender, RoutedEventArgs e) private static void HandlePasswordChanged(object sender, RoutedEventArgs e)
{ {

View File

@ -0,0 +1,26 @@
using System;
namespace PettingZoo.UI.Subscribe
{
public interface ISubscribeDialog
{
SubscribeDialogParams? Show(SubscribeDialogParams? defaultParams = null);
}
public class SubscribeDialogParams
{
public string Exchange { get; }
public string RoutingKey { get; }
public static SubscribeDialogParams Default { get; } = new("", "#");
public SubscribeDialogParams(string exchange, string routingKey)
{
Exchange = exchange;
RoutingKey = routingKey;
}
}
}

View File

@ -0,0 +1,66 @@
using System;
using System.Windows.Input;
// TODO validate input
namespace PettingZoo.UI.Subscribe
{
public class SubscribeViewModel : BaseViewModel
{
private string exchange;
private string routingKey;
public string Exchange
{
get => exchange;
set => SetField(ref exchange, value);
}
public string RoutingKey
{
get => routingKey;
set => SetField(ref routingKey, value);
}
public ICommand OkCommand { get; }
public event EventHandler? OkClick;
public SubscribeViewModel(SubscribeDialogParams subscribeParams)
{
OkCommand = new DelegateCommand(OkExecute, OkCanExecute);
exchange = subscribeParams.Exchange;
routingKey = subscribeParams.RoutingKey;
}
public SubscribeDialogParams ToModel()
{
return new(Exchange, RoutingKey);
}
private void OkExecute()
{
OkClick?.Invoke(this, EventArgs.Empty);
}
private static bool OkCanExecute()
{
return true;
}
}
public class DesignTimeSubscribeViewModel : SubscribeViewModel
{
public DesignTimeSubscribeViewModel() : base(SubscribeDialogParams.Default)
{
}
}
}

View File

@ -0,0 +1,41 @@
<Window x:Class="PettingZoo.UI.Subscribe.SubscribeWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:PettingZoo.UI"
xmlns:connection="clr-namespace:PettingZoo.UI.Connection"
xmlns:subscribe="clr-namespace:PettingZoo.UI.Subscribe"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance connection:DesignTimeConnectionViewModel, IsDesignTimeCreatable = True}"
Width="500"
SizeToContent="Height"
ResizeMode="NoResize"
WindowStyle="ToolWindow"
Style="{StaticResource WindowStyle}"
Title="{x:Static subscribe:SubscribeWindowStrings.WindowTitle}"
FocusManager.FocusedElement="{Binding ElementName=ExchangeTextBox}">
<DockPanel Margin="8">
<UniformGrid DockPanel.Dock="Bottom" HorizontalAlignment="Right" Rows="1" Columns="2" Style="{StaticResource FooterPanel}">
<Button IsDefault="True" Content="{x:Static subscribe:SubscribeWindowStrings.ButtonOK}" Style="{StaticResource FooterButton}" Command="{Binding OkCommand}"/>
<Button IsCancel="True" Content="{x:Static subscribe:SubscribeWindowStrings.ButtonCancel}" Style="{StaticResource FooterButton}"/>
</UniformGrid>
<ui:GridLayout Style="{StaticResource Form}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0" Content="{x:Static subscribe:SubscribeWindowStrings.LabelExchange}"/>
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Exchange}" Name="ExchangeTextBox"/>
<Label Grid.Column="0" Grid.Row="1" Content="{x:Static subscribe:SubscribeWindowStrings.LabelRoutingKey}"/>
<TextBox Grid.Column="1" Grid.Row="1" Text="{Binding RoutingKey}"/>
</ui:GridLayout>
</DockPanel>
</Window>

View File

@ -0,0 +1,37 @@
using System.Windows;
namespace PettingZoo.UI.Subscribe
{
public class WindowSubscribeDialog : ISubscribeDialog
{
public SubscribeDialogParams? Show(SubscribeDialogParams? defaultParams = null)
{
var viewModel = new SubscribeViewModel(defaultParams ?? SubscribeDialogParams.Default);
var window = new SubscribeWindow(viewModel)
{
Owner = Application.Current.MainWindow
};
viewModel.OkClick += (_, _) =>
{
window.DialogResult = true;
};
return window.ShowDialog().GetValueOrDefault()
? viewModel.ToModel()
: null;
}
}
public partial class SubscribeWindow
{
public SubscribeWindow(SubscribeViewModel viewModel)
{
WindowStartupLocation = WindowStartupLocation.CenterOwner;
InitializeComponent();
DataContext = viewModel;
}
}
}

View File

@ -0,0 +1,108 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PettingZoo.UI.Subscribe {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class SubscribeWindowStrings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal SubscribeWindowStrings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Subscribe.SubscribeWindowStrings", typeof(SubscribeWindowStrings).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Cancel.
/// </summary>
public static string ButtonCancel {
get {
return ResourceManager.GetString("ButtonCancel", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to OK.
/// </summary>
public static string ButtonOK {
get {
return ResourceManager.GetString("ButtonOK", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Exchange:.
/// </summary>
public static string LabelExchange {
get {
return ResourceManager.GetString("LabelExchange", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Routing key:.
/// </summary>
public static string LabelRoutingKey {
get {
return ResourceManager.GetString("LabelRoutingKey", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Subscribe parameters.
/// </summary>
public static string WindowTitle {
get {
return ResourceManager.GetString("WindowTitle", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ButtonCancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="ButtonOK" xml:space="preserve">
<value>OK</value>
</data>
<data name="LabelExchange" xml:space="preserve">
<value>Exchange:</value>
</data>
<data name="LabelRoutingKey" xml:space="preserve">
<value>Routing key:</value>
</data>
<data name="WindowTitle" xml:space="preserve">
<value>Subscribe parameters</value>
</data>
</root>

View File

@ -0,0 +1,37 @@
using System;
using System.Windows;
using System.Windows.Media;
using SharpVectors.Dom.Svg;
using SharpVectors.Renderers.Utils;
using SharpVectors.Renderers.Wpf;
namespace PettingZoo.UI
{
public static class SvgIconHelper
{
public static ImageSource LoadFromResource(string resourceName)
{
var streamInfo = Application.GetResourceStream(new Uri($"/PettingZoo;component{resourceName}", UriKind.Relative));
if (streamInfo == null)
throw new ArgumentException($"Resource '{resourceName}' not found");
// Basically the code used in FileSvgConverter, but that only supports outputting to a file not returning the Drawing
var wpfDrawingSettings = new WpfDrawingSettings
{
IncludeRuntime = true,
TextAsGeometry = true
};
var wpfRenderer = new WpfDrawingRenderer(wpfDrawingSettings);
var wpfWindow = new WpfSvgWindow(0, 0, wpfRenderer);
wpfWindow.LoadDocument(streamInfo.Stream, wpfDrawingSettings);
wpfRenderer.InvalidRect = SvgRectF.Empty;
wpfRenderer.Render(wpfWindow.Document);
if (wpfRenderer.Drawing == null)
throw new ArgumentException($"Resource '{resourceName}' is not a valid SVG");
return new DrawingImage(wpfRenderer.Drawing);
}
}
}

37
PettingZoo/UI/Tab/ITab.cs Normal file
View File

@ -0,0 +1,37 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace PettingZoo.UI.Tab
{
public interface ITabToolbarCommands : INotifyPropertyChanged
{
IEnumerable<TabToolbarCommand> ToolbarCommands { get; }
}
public interface ITab : ITabToolbarCommands
{
string Title { get; }
ContentControl Content { get; }
ICommand CloseTabCommand { get; }
}
public readonly struct TabToolbarCommand
{
public ICommand Command { get; }
public string Caption { get; }
public ImageSource Icon { get; }
public TabToolbarCommand(ICommand command, string caption, ImageSource icon)
{
Command = command;
Caption = caption;
Icon = icon;
}
}
}

View File

@ -0,0 +1,15 @@
using System.Windows.Input;
using PettingZoo.Core.Connection;
namespace PettingZoo.UI.Tab
{
// Passing the closeTabCommand is necessary because I haven't figured out how to bind the main window's
// context menu items for the tab to the main window's datacontext yet. RelativeSource doesn't seem to work
// because the popup is it's own window. Refactor if a better solution is found.
public interface ITabFactory
{
ITab CreateSubscriberTab(ICommand closeTabCommand, ISubscriber subscriber);
ITab CreatePublisherTab(ICommand closeTabCommand, IConnection connection);
}
}

View File

@ -0,0 +1,21 @@
<UserControl x:Class="PettingZoo.UI.Tab.Publisher.PublisherView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:res="clr-namespace:PettingZoo.UI.Tab.Publisher"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance res:DesignTimePublisherViewModel, IsDesignTimeCreatable=True}"
Background="White">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Static res:PublisherViewStrings.LabelMessageType}" Style="{StaticResource TypeSelection}" />
<RadioButton Content="{x:Static res:PublisherViewStrings.OptionMessageTypeRaw}" Style="{StaticResource TypeSelection}" IsChecked="{Binding MessageTypeRaw}" />
<RadioButton Content="{x:Static res:PublisherViewStrings.OptionMessageTypeTapeti}" Style="{StaticResource TypeSelection}" IsChecked="{Binding MessageTypeTapeti}" />
</StackPanel>
<TextBlock>TODO: implement publish forms</TextBlock>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,19 @@
using System.Windows.Media;
namespace PettingZoo.UI.Tab.Publisher
{
/// <summary>
/// Interaction logic for PublisherView.xaml
/// </summary>
public partial class PublisherView
{
public PublisherView(PublisherViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
Background = Brushes.Transparent;
}
}
}

View File

@ -0,0 +1,90 @@
using System.Collections.Generic;
using System.Windows.Input;
using PettingZoo.Core.Connection;
namespace PettingZoo.UI.Tab.Publisher
{
public enum MessageType
{
Raw,
Tapeti
}
public class PublisherViewModel : BaseViewModel, ITabToolbarCommands
{
private readonly IConnection connection;
private MessageType messageType;
private readonly DelegateCommand publishCommand;
private readonly TabToolbarCommand[] toolbarCommands;
public MessageType MessageType
{
get => messageType;
set => SetField(ref messageType, value,
otherPropertiesChanged: new[]
{
nameof(MessageTypeRaw),
nameof(MessageTypeTapeti)
});
}
public bool MessageTypeRaw
{
get => MessageType == MessageType.Raw;
set { if (value) MessageType = MessageType.Raw; }
}
public bool MessageTypeTapeti
{
get => MessageType == MessageType.Tapeti;
set { if (value) MessageType = MessageType.Tapeti; }
}
public ICommand PublishCommand => publishCommand;
// TODO make more dynamic, include entered routing key for example
public string Title => "Publish";
public IEnumerable<TabToolbarCommand> ToolbarCommands => toolbarCommands;
public PublisherViewModel(IConnection connection)
{
this.connection = connection;
publishCommand = new DelegateCommand(PublishExecute, PublishCanExecute);
toolbarCommands = new[]
{
new TabToolbarCommand(PublishCommand, PublisherViewStrings.CommandPublish, SvgIconHelper.LoadFromResource("/Images/PublishSend.svg"))
};
}
private void PublishExecute()
{
// TODO
}
private bool PublishCanExecute()
{
// TODO validate input
return true;
}
}
public class DesignTimePublisherViewModel : PublisherViewModel
{
public DesignTimePublisherViewModel() : base(null!)
{
}
}
}

View File

@ -0,0 +1,99 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PettingZoo.UI.Tab.Publisher {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class PublisherViewStrings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal PublisherViewStrings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Tab.Publisher.PublisherViewStrings", typeof(PublisherViewStrings).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Publish.
/// </summary>
public static string CommandPublish {
get {
return ResourceManager.GetString("CommandPublish", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Message type: .
/// </summary>
public static string LabelMessageType {
get {
return ResourceManager.GetString("LabelMessageType", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Raw.
/// </summary>
public static string OptionMessageTypeRaw {
get {
return ResourceManager.GetString("OptionMessageTypeRaw", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tapeti.
/// </summary>
public static string OptionMessageTypeTapeti {
get {
return ResourceManager.GetString("OptionMessageTypeTapeti", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CommandPublish" xml:space="preserve">
<value>Publish</value>
</data>
<data name="LabelMessageType" xml:space="preserve">
<value>Message type: </value>
</data>
<data name="OptionMessageTypeRaw" xml:space="preserve">
<value>Raw</value>
</data>
<data name="OptionMessageTypeTapeti" xml:space="preserve">
<value>Tapeti</value>
</data>
</root>

View File

@ -0,0 +1,69 @@
<UserControl x:Class="PettingZoo.UI.Tab.Subscriber.SubscriberView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:ui="clr-namespace:PettingZoo.UI"
xmlns:res="clr-namespace:PettingZoo.UI.Tab.Subscriber"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800"
d:DataContext="{d:DesignInstance res:DesignTimeSubscriberViewModel, IsDesignTimeCreatable=True}"
Background="White">
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Messages}"
SelectedItem="{Binding Path=SelectedMessage, Mode=TwoWay}"
ui:ListBox.AutoScroll="True">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding Timestamp, StringFormat=g}" Style="{StaticResource Timestamp}"></TextBlock>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding RoutingKey}" Style="{StaticResource Routingkey}"></TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Width="5" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch"/>
<Grid Grid.Column="2" Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="200"/>
</Grid.RowDefinitions>
<Border Grid.Column="0" Grid.Row="0" Style="{StaticResource SidePanel}">
<DockPanel>
<Label DockPanel.Dock="Top" Style="{StaticResource HeaderLabel}" Content="{x:Static res:SubscriberViewStrings.PanelTitleBody}"/>
<TextBox
Text="{Binding SelectedMessageBody, Mode=OneWay}"
BorderThickness="0"
IsReadOnly="True"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"/>
</DockPanel>
</Border>
<GridSplitter HorizontalAlignment="Stretch" Grid.Column="0" Grid.Row="1" Height="5" ResizeDirection="Rows"/>
<Border Grid.Column="0" Grid.Row="2" Style="{StaticResource SidePanel}">
<DockPanel>
<Label DockPanel.Dock="Top" Style="{StaticResource HeaderLabel}" Content="{x:Static res:SubscriberViewStrings.PanelTitleProperties}"/>
<DataGrid ItemsSource="{Binding SelectedMessageProperties}" AutoGenerateColumns="False" IsReadOnly="True" Style="{StaticResource Properties}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Key}" Header="{x:Static res:SubscriberViewStrings.PropertyName}" Width="100"/>
<DataGridTextColumn Binding="{Binding Value}" Header="{x:Static res:SubscriberViewStrings.PropertyValue}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Border>
</Grid>
</Grid>
</UserControl>

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Windows.Media;
namespace PettingZoo.UI.Tab.Subscriber
{
/// <summary>
/// Interaction logic for SubscriberView.xaml
/// </summary>
public partial class SubscriberView
{
public SubscriberView(SubscriberViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
if (!System.ComponentModel.DesignerProperties.GetIsInDesignMode(this))
Background = Brushes.Transparent;
}
}
}

View File

@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using PettingZoo.Core.Connection;
using PettingZoo.Core.Rendering;
using PettingZoo.RabbitMQ;
// TODO update title with unread message count if tab is not active
namespace PettingZoo.UI.Tab.Subscriber
{
public class SubscriberViewModel : BaseViewModel, ITabToolbarCommands
{
private readonly ISubscriber subscriber;
private readonly TaskScheduler uiScheduler;
private MessageInfo? selectedMessage;
private readonly DelegateCommand clearCommand;
private readonly TabToolbarCommand[] toolbarCommands;
public ICommand ClearCommand => clearCommand;
public ObservableCollection<MessageInfo> Messages { get; }
public MessageInfo? SelectedMessage
{
get => selectedMessage;
set
{
if (value == selectedMessage)
return;
selectedMessage = value;
RaisePropertyChanged();
RaiseOtherPropertyChanged(nameof(SelectedMessageBody));
RaiseOtherPropertyChanged(nameof(SelectedMessageProperties));
}
}
public string SelectedMessageBody =>
SelectedMessage != null
? MessageBodyRenderer.Render(SelectedMessage.Body, SelectedMessage.Properties.ContentType())
: "";
public IDictionary<string, string>? SelectedMessageProperties => SelectedMessage?.Properties;
public string Title => $"{subscriber.Exchange} - {subscriber.RoutingKey}";
public IEnumerable<TabToolbarCommand> ToolbarCommands => toolbarCommands;
public SubscriberViewModel(ISubscriber subscriber)
{
this.subscriber = subscriber;
uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
Messages = new ObservableCollection<MessageInfo>();
clearCommand = new DelegateCommand(ClearExecute, ClearCanExecute);
toolbarCommands = new[]
{
new TabToolbarCommand(ClearCommand, SubscriberViewStrings.CommandClear, SvgIconHelper.LoadFromResource("/Images/Clear.svg"))
};
subscriber.MessageReceived += SubscriberMessageReceived;
subscriber.Start();
}
private void ClearExecute()
{
Messages.Clear();
clearCommand.RaiseCanExecuteChanged();
}
private bool ClearCanExecute()
{
return Messages.Count > 0;
}
private void SubscriberMessageReceived(object? sender, MessageReceivedEventArgs args)
{
RunFromUiScheduler(() =>
{
Messages.Add(args.MessageInfo);
clearCommand.RaiseCanExecuteChanged();
});
}
private void RunFromUiScheduler(Action action)
{
_ = Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
}
}
public class DesignTimeSubscriberViewModel : SubscriberViewModel
{
public DesignTimeSubscriberViewModel() : base(new DesignTimeSubscriber())
{
}
private class DesignTimeSubscriber : ISubscriber
{
public ValueTask DisposeAsync()
{
return default;
}
public string Exchange { get; } = "dummy";
public string RoutingKey { get; } = "dummy";
#pragma warning disable CS0067
public event EventHandler<MessageReceivedEventArgs>? MessageReceived;
#pragma warning restore CS0067
public void Start()
{
}
}
}
}

View File

@ -0,0 +1,126 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace PettingZoo.UI.Tab.Subscriber {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class SubscriberViewStrings {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal SubscriberViewStrings() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PettingZoo.UI.Tab.Subscriber.SubscriberViewStrings", typeof(SubscriberViewStrings).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized string similar to Clear.
/// </summary>
public static string CommandClear {
get {
return ResourceManager.GetString("CommandClear", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Non-persistent.
/// </summary>
public static string DeliveryModeNonPersistent {
get {
return ResourceManager.GetString("DeliveryModeNonPersistent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Persistent.
/// </summary>
public static string DeliveryModePersistent {
get {
return ResourceManager.GetString("DeliveryModePersistent", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Body.
/// </summary>
public static string PanelTitleBody {
get {
return ResourceManager.GetString("PanelTitleBody", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Properties.
/// </summary>
public static string PanelTitleProperties {
get {
return ResourceManager.GetString("PanelTitleProperties", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Name.
/// </summary>
public static string PropertyName {
get {
return ResourceManager.GetString("PropertyName", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Value.
/// </summary>
public static string PropertyValue {
get {
return ResourceManager.GetString("PropertyValue", resourceCulture);
}
}
}
}

View File

@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="CommandClear" xml:space="preserve">
<value>Clear</value>
</data>
<data name="DeliveryModeNonPersistent" xml:space="preserve">
<value>Non-persistent</value>
</data>
<data name="DeliveryModePersistent" xml:space="preserve">
<value>Persistent</value>
</data>
<data name="PanelTitleBody" xml:space="preserve">
<value>Body</value>
</data>
<data name="PanelTitleProperties" xml:space="preserve">
<value>Properties</value>
</data>
<data name="PropertyName" xml:space="preserve">
<value>Name</value>
</data>
<data name="PropertyValue" xml:space="preserve">
<value>Value</value>
</data>
</root>

View File

@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Windows.Controls;
using System.Windows.Input;
namespace PettingZoo.UI.Tab
{
public class ViewTab<TView, TViewModel> : ITab where TView : ContentControl where TViewModel : INotifyPropertyChanged
{
public string Title => getTitle(viewModel);
public ContentControl Content { get; }
public ICommand CloseTabCommand { get; }
public IEnumerable<TabToolbarCommand> ToolbarCommands => viewModel is ITabToolbarCommands tabToolbarCommands
? tabToolbarCommands.ToolbarCommands
: Enumerable.Empty<TabToolbarCommand>();
public event PropertyChangedEventHandler? PropertyChanged;
private readonly TViewModel viewModel;
private readonly Func<TViewModel, string> getTitle;
public ViewTab(ICommand closeTabCommand, TView view, TViewModel viewModel, Expression<Func<TViewModel, string>> title)
{
if (title.Body is not MemberExpression titleMemberExpression)
throw new ArgumentException(@"Invalid expression type, expected viewModel => viewModel.TitlePropertyName", nameof(title));
var titlePropertyName = titleMemberExpression.Member.Name;
CloseTabCommand = closeTabCommand;
this.viewModel = viewModel;
getTitle = title.Compile();
Content = view;
viewModel.PropertyChanged += (_, args) =>
{
if (args.PropertyName == titlePropertyName)
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Title)));
else if (args.PropertyName == nameof(ToolbarCommands))
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ToolbarCommands)));
};
}
}
}

View File

@ -0,0 +1,31 @@
using System.Windows.Input;
using PettingZoo.Core.Connection;
using PettingZoo.UI.Tab.Publisher;
using PettingZoo.UI.Tab.Subscriber;
namespace PettingZoo.UI.Tab
{
public class ViewTabFactory : ITabFactory
{
public ITab CreateSubscriberTab(ICommand closeTabCommand, ISubscriber subscriber)
{
var viewModel = new SubscriberViewModel(subscriber);
return new ViewTab<SubscriberView, SubscriberViewModel>(
closeTabCommand,
new SubscriberView(viewModel),
viewModel,
vm => vm.Title);
}
public ITab CreatePublisherTab(ICommand closeTabCommand, IConnection connection)
{
var viewModel = new PublisherViewModel(connection);
return new ViewTab<PublisherView, PublisherViewModel>(
closeTabCommand,
new PublisherView(viewModel),
viewModel,
vm => vm.Title);
}
}
}

View File

@ -1,54 +0,0 @@
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Petting Zoo")]
[assembly: AssemblyDescription("Petting Zoo - a RabbitMQ message viewer")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("X²Software")]
[assembly: AssemblyProduct("Petting Zoo")]
[assembly: AssemblyCopyright("Copyright © 2016")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
//In order to begin building localizable applications, set
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> to en-US. Then uncomment
//the NeutralResourceLanguage attribute below. Update the "en-US" in
//the line below to match the UICulture setting in the project file.
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: GuidAttribute("1739c968-ca2a-4293-b487-bfa193a7caf3")]

View File

@ -1,72 +0,0 @@
using System.Windows;
using System.Windows.Input;
using AutoMapper;
using PettingZoo.Model;
using PettingZoo.ViewModel;
namespace PettingZoo.View
{
public class WindowConnectionInfoBuilder : IConnectionInfoBuilder
{
private readonly UserSettings userSettings;
private static readonly IMapper ConnectionInfoMapper = new MapperConfiguration(cfg =>
{
cfg.RecognizeDestinationPrefixes("Last");
cfg.RecognizePrefixes("Last");
cfg.CreateMap<ConnectionInfo, ConnectionWindowSettings>().ReverseMap();
}).CreateMapper();
public WindowConnectionInfoBuilder(UserSettings userSettings)
{
this.userSettings = userSettings;
}
public ConnectionInfo Build()
{
var connectionInfo = ConnectionInfoMapper.Map<ConnectionInfo>(userSettings.ConnectionWindow);
var viewModel = new ConnectionViewModel(connectionInfo);
var dialog = new ConnectionWindow(viewModel)
{
Owner = Application.Current.MainWindow
};
viewModel.CloseWindow += (sender, args) =>
{
dialog.DialogResult = true;
};
connectionInfo = dialog.ShowDialog().GetValueOrDefault() ? viewModel.ToModel() : null;
if (connectionInfo != null)
{
ConnectionInfoMapper.Map(connectionInfo, userSettings.ConnectionWindow);
userSettings.Save();
}
return connectionInfo;
}
}
public partial class ConnectionWindow
{
public ConnectionWindow(ConnectionViewModel viewModel)
{
WindowStartupLocation = WindowStartupLocation.CenterOwner;
InitializeComponent();
DataContext = viewModel;
}
private void NumericPreviewTextInput(object sender, TextCompositionEventArgs args)
{
if (!char.IsDigit(args.Text, args.Text.Length - 1))
args.Handled = true;
}
}
}

View File

@ -1,100 +0,0 @@
<Window x:Class="PettingZoo.View.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:res="clr-namespace:PettingZoo.Properties"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:viewModel="clr-namespace:PettingZoo.ViewModel"
xmlns:infrastructure="clr-namespace:PettingZoo.Infrastructure"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance viewModel:MainViewModel}"
Width="800"
Height="600"
ResizeMode="CanResizeWithGrip"
Style="{StaticResource WindowStyle}"
Title="{x:Static res:Resources.MainWindowTitle}">
<DockPanel>
<ToolBar DockPanel.Dock="Top" ToolBarTray.IsLocked="True">
<Button Command="{Binding ConnectCommand}">
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource ConnectIcon}" Width="16" Height="16" Style="{StaticResource ToolbarIcon}"/>
<TextBlock Margin="3,0,0,0">Connect</TextBlock>
</StackPanel>
</Button>
<Button Command="{Binding DisconnectCommand}">
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource DisconnectIcon}" Width="16" Height="16" Style="{StaticResource ToolbarIcon}"/>
<TextBlock Margin="3,0,0,0">Disconnect</TextBlock>
</StackPanel>
</Button>
<Separator Style="{StaticResource {x:Static ToolBar.SeparatorStyleKey}}" />
<Button Command="{Binding ClearCommand}">
<StackPanel Orientation="Horizontal">
<Image Source="{StaticResource ClearIcon}" Width="16" Height="16" Style="{StaticResource ToolbarIcon}"/>
<TextBlock Margin="3,0,0,0">Clear messages</TextBlock>
</StackPanel>
</Button>
</ToolBar>
<StatusBar DockPanel.Dock="Bottom">
<StatusBarItem>
<TextBlock Text="{Binding ConnectionStatus}"/>
</StatusBarItem>
</StatusBar>
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListBox Grid.Column="0" Grid.Row="0"
HorizontalAlignment="Stretch"
ItemsSource="{Binding Messages}"
SelectedItem="{Binding Path=SelectedMessage, Mode=TwoWay}"
infrastructure:ListBox.AutoScroll="True">
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Text="{Binding Timestamp, StringFormat=g}" Style="{StaticResource Timestamp}"></TextBlock>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding RoutingKey}" Style="{StaticResource Routingkey}"></TextBlock>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<GridSplitter Width="5" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch"/>
<Grid Grid.Column="2" Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="200"/>
</Grid.RowDefinitions>
<Border Grid.Column="0" Grid.Row="0" Style="{StaticResource SidePanel}">
<DockPanel>
<Label DockPanel.Dock="Top" Style="{StaticResource HeaderLabel}" Content="{x:Static res:Resources.PanelTitleBody}"/>
<TextBox
Text="{Binding SelectedMessageBody, Mode=OneWay}"
BorderThickness="0"
IsReadOnly="True"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"/>
</DockPanel>
</Border>
<GridSplitter HorizontalAlignment="Stretch" Grid.Column="0" Grid.Row="1" Height="5" ResizeDirection="Rows"/>
<Border Grid.Column="0" Grid.Row="2" Style="{StaticResource SidePanel}">
<DockPanel>
<Label DockPanel.Dock="Top" Style="{StaticResource HeaderLabel}" Content="{x:Static res:Resources.PanelTitleProperties}"/>
<DataGrid ItemsSource="{Binding SelectedMessageProperties}" AutoGenerateColumns="False" IsReadOnly="True" Style="{StaticResource Properties}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Key}" Header="{x:Static res:Resources.PropertyName}" Width="100"/>
<DataGridTextColumn Binding="{Binding Value}" Header="{x:Static res:Resources.PropertyValue}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Border>
</Grid>
</Grid>
</DockPanel>
</Window>

View File

@ -1,27 +0,0 @@
using System;
using System.Windows;
using PettingZoo.ViewModel;
namespace PettingZoo.View
{
public partial class MainWindow
{
public MainWindow(MainViewModel viewModel)
{
WindowStartupLocation = WindowStartupLocation.CenterScreen;
InitializeComponent();
DataContext = viewModel;
Dispatcher.ShutdownStarted += OnDispatcherShutDownStarted;
}
private void OnDispatcherShutDownStarted(object sender, EventArgs e)
{
var disposable = DataContext as IDisposable;
if (!ReferenceEquals(null, disposable))
disposable.Dispose();
}
}
}

View File

@ -1,64 +0,0 @@
using System;
using System.Windows.Input;
using AutoMapper;
using PettingZoo.Infrastructure;
using PettingZoo.Model;
namespace PettingZoo.ViewModel
{
public class ConnectionViewModel : BaseViewModel
{
private static readonly IMapper ModelMapper = new MapperConfiguration(cfg =>
cfg.CreateMap<ConnectionInfo, ConnectionViewModel>().ReverseMap()
).CreateMapper();
private readonly DelegateCommand okCommand;
public string Host { get; set; }
public string VirtualHost { get; set; }
public int Port { get; set; }
public string Username { get; set; }
public string Password { get; set; }
public string Exchange { get; set; }
public string RoutingKey { get; set; }
public ICommand OkCommand { get { return okCommand; } }
public event EventHandler CloseWindow;
public ConnectionViewModel()
{
okCommand = new DelegateCommand(OkExecute, OkCanExecute);
}
public ConnectionViewModel(ConnectionInfo model) : this()
{
ModelMapper.Map(model, this);
}
public ConnectionInfo ToModel()
{
return ModelMapper.Map<ConnectionInfo>(this);
}
private void OkExecute()
{
if (CloseWindow != null)
CloseWindow(this, EventArgs.Empty);
}
private bool OkCanExecute()
{
return true;
}
}
}

View File

@ -1,210 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using PettingZoo.Infrastructure;
using PettingZoo.Model;
using PettingZoo.Properties;
namespace PettingZoo.ViewModel
{
public class MainViewModel : BaseViewModel, IDisposable
{
private readonly TaskScheduler uiScheduler;
private readonly IConnectionInfoBuilder connectionInfoBuilder;
private readonly IConnectionFactory connectionFactory;
private ConnectionInfo connectionInfo;
private IConnection connection;
private string connectionStatus;
private readonly ObservableCollection<MessageInfo> messages;
private MessageInfo selectedMessage;
private readonly DelegateCommand connectCommand;
private readonly DelegateCommand disconnectCommand;
private readonly DelegateCommand clearCommand;
public ConnectionInfo ConnectionInfo {
get { return connectionInfo; }
private set
{
if (value == connectionInfo)
return;
connectionInfo = value;
RaisePropertyChanged();
}
}
public string ConnectionStatus
{
get { return connectionStatus; }
private set
{
if (value == connectionStatus)
return;
connectionStatus = value;
RaisePropertyChanged();
}
}
public ObservableCollection<MessageInfo> Messages { get { return messages; } }
public MessageInfo SelectedMessage
{
get { return selectedMessage; }
set
{
if (value == selectedMessage)
return;
selectedMessage = value;
RaisePropertyChanged();
RaiseOtherPropertyChanged("SelectedMessageBody");
RaiseOtherPropertyChanged("SelectedMessageProperties");
}
}
public string SelectedMessageBody
{
get
{
return SelectedMessage != null
? MessageBodyRenderer.Render(SelectedMessage.Body, SelectedMessage.ContentType)
: "";
}
}
public Dictionary<string, string> SelectedMessageProperties
{
get { return SelectedMessage != null ? SelectedMessage.Properties : null; }
}
public ICommand ConnectCommand { get { return connectCommand; } }
public ICommand DisconnectCommand { get { return disconnectCommand; } }
public ICommand ClearCommand { get { return clearCommand; } }
public MainViewModel(IConnectionInfoBuilder connectionInfoBuilder, IConnectionFactory connectionFactory)
{
uiScheduler = TaskScheduler.FromCurrentSynchronizationContext();
this.connectionInfoBuilder = connectionInfoBuilder;
this.connectionFactory = connectionFactory;
connectionStatus = GetConnectionStatus(null);
messages = new ObservableCollection<MessageInfo>();
connectCommand = new DelegateCommand(ConnectExecute);
disconnectCommand = new DelegateCommand(DisconnectExecute, DisconnectCanExecute);
clearCommand = new DelegateCommand(ClearExecute, ClearCanExecute);
}
public void Dispose()
{
if (connection != null)
{
connection.Dispose();
connection = null;
}
}
private void ConnectExecute()
{
var newInfo = connectionInfoBuilder.Build();
if (newInfo == null)
return;
if (connection != null)
connection.Dispose();
ConnectionInfo = newInfo;
connection = connectionFactory.CreateConnection(connectionInfo);
connection.MessageReceived += ConnectionMessageReceived;
connection.StatusChanged += ConnectionStatusChanged;
disconnectCommand.RaiseCanExecuteChanged();
}
private void DisconnectExecute()
{
if (connection != null)
{
connection.Dispose();
connection = null;
}
ConnectionInfo = null;
ConnectionStatus = GetConnectionStatus(null);
disconnectCommand.RaiseCanExecuteChanged();
}
private bool DisconnectCanExecute()
{
return connection != null;
}
private void ClearExecute()
{
messages.Clear();
clearCommand.RaiseCanExecuteChanged();
}
private bool ClearCanExecute()
{
return messages.Count > 0;
}
private void ConnectionStatusChanged(object sender, StatusChangedEventArgs args)
{
ConnectionStatus = GetConnectionStatus(args);
}
private void ConnectionMessageReceived(object sender, MessageReceivedEventArgs args)
{
RunFromUiScheduler(() =>
{
messages.Add(args.MessageInfo);
clearCommand.RaiseCanExecuteChanged();
});
}
private string GetConnectionStatus(StatusChangedEventArgs args)
{
if (args != null)
switch (args.Status)
{
case Model.ConnectionStatus.Connecting:
return String.Format(Resources.StatusConnecting, args.Context);
case Model.ConnectionStatus.Connected:
return String.Format(Resources.StatusConnected, args.Context);
case Model.ConnectionStatus.Error:
return String.Format(Resources.StatusError, args.Context);
}
return Resources.StatusDisconnected;
}
private void RunFromUiScheduler(Action action)
{
Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, uiScheduler);
}
}
}

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="AutoMapper" version="4.2.1" targetFramework="net45" />
<package id="Newtonsoft.Json" version="8.0.3" targetFramework="net45" />
<package id="RabbitMQ.Client" version="3.6.2" targetFramework="net45" />
<package id="SimpleInjector" version="3.1.5" targetFramework="net45" />
</packages>