Click here to Skip to main content
15,896,501 members
Articles / Desktop Programming / WPF

NetworkView: A WPF custom control for visualizing and editing networks, graphs and flow-charts

Rate me:
Please Sign up or sign in to vote.
4.97/5 (176 votes)
1 Sep 2019MIT51 min read 557.1K   18.6K   421  
This article examines the use and implementation of a WPF custom control that is used to display and edit networks, graphs and flow-charts.
 <Window x:Class="SampleCode.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SampleCode"
    xmlns:NetworkUI="clr-namespace:NetworkUI;assembly=NetworkUI"
    xmlns:NetworkModel="clr-namespace:NetworkModel;assembly=NetworkModel"
    xmlns:ZoomAndPan="clr-namespace:ZoomAndPan;assembly=ZoomAndPan"
    xmlns:ac="clr-namespace:AdornedControl;assembly=AdornedControl"
    xmlns:diagnostics="clr-namespace:System.Diagnostics;assembly=WindowsBase"
    Title="Main Window" 
    x:Name="mainWindow"
    MinWidth="400"
    MinHeight="150"
	Width="800"
    Height="500"
	Loaded="MainWindow_Loaded"
    FocusManager.FocusedElement="{Binding ElementName=networkControl}"
    >
   
    <!-- 
    Including this in binding statements for diagnostics:
    diagnostics:PresentationTraceSources.TraceLevel=High
    -->

    <Window.Resources>

        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>                
                <!-- 
                Merge in the resource dictionary that is shared between the main window and the overview window.
                -->
                <ResourceDictionary 
                    Source="SharedVisualTemplates.xaml"
                    />
                
            </ResourceDictionary.MergedDictionaries>

            <!-- UI commands. -->
            
            <RoutedUICommand x:Key="Commands.DeleteSelectedNodes" />
            <RoutedUICommand x:Key="Commands.CreateNode" />
            <RoutedUICommand x:Key="Commands.DeleteNode" />
            <RoutedUICommand x:Key="Commands.DeleteConnection" />            
            <RoutedUICommand x:Key="Commands.ZoomOut" />
            <RoutedUICommand x:Key="Commands.ZoomIn" />
            <RoutedUICommand x:Key="Commands.JumpBackToPrevZoom" />
            <RoutedUICommand x:Key="Commands.FitContent" />
            <RoutedUICommand x:Key="Commands.Fill" />
            <RoutedUICommand x:Key="Commands.OneHundredPercent" />

            <!-- 
            This converts from a scale value to a percentage value.
            It is used to convert the value of 'ContentScale' to the percentage zoom level that is displayed in the UI.
            -->
            <local:ScaleToPercentConverter 
                x:Key="scaleToPercentConverter" 
                />

            <!-- 
            This graphic is used to indicate that a connection cannot be made between two particular connectors.        
            -->
            <DataTemplate DataType="{x:Type local:ConnectionBadIndicator}">
                <Grid
                    Width="80"
                    >
                    <Image 
                        Width="32"
                        Height="32"
                        Source="Resources/block_16.png"
                        HorizontalAlignment="Right"
                        />
                </Grid>
            </DataTemplate>

            <!-- 
            This graphic is used to indicate that a connection can be made between two particular connectors.        
            -->
            <DataTemplate DataType="{x:Type local:ConnectionOkIndicator}">
                <Grid
                    Width="80"
                    >
                    <Image 
                        Width="32"
                        Height="32"
                        Source="Resources/tick_16.png"
                        HorizontalAlignment="Right"
                        />
                </Grid>
            </DataTemplate>
                 
            <!-- 
            Define the visual style for a 'ConnectorItem'.
            -->
            <Style 
                TargetType="{x:Type NetworkUI:ConnectorItem}"
                >
                <!-- 
                Data-binding for the connector hotspot.
                ConnectorItem automatically computes its center points and assings this value
                to the 'Hotspot' property.  This data-binding then 'pushes' the value into the application
                view-model.
                -->
                <Setter 
                    Property="Hotspot"
                    Value="{Binding Hotspot, Mode=OneWayToSource}"
                    />
                
                <!-- The visual template. -->
                <Setter 
                    Property="Template"
                    >
                    <Setter.Value>
                        <ControlTemplate 
                            TargetType="{x:Type NetworkUI:ConnectorItem}"
                            >
                            <!-- The visual for the connector. -->
                            <Ellipse
                                Stroke="{StaticResource nodeBorderBrush}"
                                Fill="{StaticResource connectorBackgroundBrush}"
                                />
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>     
            
            <!-- 
            Template for the button that is used to delete nodes and connections in the graph.
            This button is displayed in an adorner when the user hovers the mouse over a node or connection.
            -->
            <ControlTemplate 
                x:Key="deleteButtonTemplate"
                TargetType="{x:Type Button}"
                >
                <Grid
                    x:Name="grid"
                    >
                    <Grid.RenderTransform>
                        <ScaleTransform
                            ScaleX="1"
                            ScaleY="1"
                            CenterX="10"
                            CenterY="10"
                            />
                    </Grid.RenderTransform>
                    <Ellipse
                        x:Name="shadow"
                        VerticalAlignment="Stretch"
                        HorizontalAlignment="Stretch"
                        Fill="Gray"
                        >
                        <Ellipse.RenderTransform>
                            <TranslateTransform
                                X="1.5"
                                Y="1.5"
                                />
                        </Ellipse.RenderTransform>                                                            
                    </Ellipse>
                    <Ellipse
                        x:Name="ellipse"
                        Stroke="Black"
                        VerticalAlignment="Stretch"
                        HorizontalAlignment="Stretch"
                        Fill="White"
                        />
                    <Image
                        Source="Resources\scissors.png" 
                        Margin="2"
                        />
                </Grid>
                <ControlTemplate.Triggers>                                
                    <EventTrigger
                        RoutedEvent="Mouse.MouseEnter"
                        >
                        <!-- 
                        Make the 'delete connection button' larger when the mouse 
                        cursor is hovered over it.
                        -->
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetName="grid"
                                    Storyboard.TargetProperty="RenderTransform.ScaleX"
                                    To="1.3"
                                    Duration="0:0:0.25"
                                    />
                                <DoubleAnimation
                                    Storyboard.TargetName="grid"
                                    Storyboard.TargetProperty="RenderTransform.ScaleY"
                                    To="1.3"
                                    Duration="0:0:0.25"
                                    />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>

                    <EventTrigger
                        RoutedEvent="Mouse.MouseLeave"
                        >
                        <!-- 
                        Return the 'delete connection button' to normal size when the mouse
                        cursor is moved away.
                        -->
                        <BeginStoryboard>
                            <Storyboard>
                                <DoubleAnimation
                                    Storyboard.TargetName="grid"
                                    Storyboard.TargetProperty="RenderTransform.ScaleX"
                                    To="1"
                                    Duration="0:0:0.05"
                                    />
                                <DoubleAnimation
                                    Storyboard.TargetName="grid"
                                    Storyboard.TargetProperty="RenderTransform.ScaleY"
                                    To="1"
                                    Duration="0:0:0.05"
                                    />
                            </Storyboard>
                        </BeginStoryboard>
                    </EventTrigger>
                </ControlTemplate.Triggers>

            </ControlTemplate>
            
            <!-- 
            Data-template for ConectionViewModel.
            
            Note that the 'Start' and 'End' of the arrow are bound to 'SourceConnectorHotspot' and 'DestConnectorHotspot' in 
            the view-model.

            In this sample a curved arrow represents connections between nodes.
            -->
            <DataTemplate
                DataType="{x:Type NetworkModel:ConnectionViewModel}"
                >

                <!-- 
                An adorned control is used, to represent the connection. 
                When the user hovers the mouse cursor over the connection, the 
                'delete connection' adorner pops up and allows them to delete the connection.
                -->
                <ac:AdornedControl
                    HorizontalAdornerPlacement="Mouse"
                    VerticalAdornerPlacement="Mouse"
                    IsMouseOverShowEnabled="{Binding ElementName=networkControl, Path=IsNotDragging}"
                    >

                    <!-- The connection is represented by a curved arrow. -->                                
                    <local:CurvedArrow
                        Stroke="{StaticResource connectionBrush}"
                        StrokeThickness="2"
                        Fill="{StaticResource connectionBrush}"
                        Points="{Binding Points}"
                        />

                    <ac:AdornedControl.AdornerContent>

                        <!-- 
                        This is the adorner that pops up when the user hovers the mouse over the connection.
                        It displays a button that the user can click to delete the connection.
                        -->
                        <Canvas
                            x:Name="connectionAdornerCanvas"
                            HorizontalAlignment="Right"
                            VerticalAlignment="Top"
                            Width="30"
                            Height="30"
                            >
                            <Line
                                X1="0"
                                Y1="30"
                                X2="15"
                                Y2="15"
                                Stroke="Black"
                                StrokeThickness="1"
                                />
                            <Button
                                x:Name="deleteConnectionButton"
                                Canvas.Left="10"
                                Canvas.Top="0"
                                Width="20"
                                Height="20"
                                Cursor="Hand"
                                Focusable="False"
                                Command="{StaticResource Commands.DeleteConnection}"
                                CommandParameter="{Binding}"
                                Template="{StaticResource deleteButtonTemplate}"
                                />
                        </Canvas>
                    </ac:AdornedControl.AdornerContent>
                </ac:AdornedControl>
            </DataTemplate>

            <!-- Define a data-template for the 'NodeViewModel' class. -->    
            <DataTemplate
                DataType="{x:Type NetworkModel:NodeViewModel}"
                >

                <!-- 
                An adorned control is used, to represent the node. 
                When the user hovers the mouse cursor over the node, the 
                'delete node' adorner pops up and allows them to delete the node.
                -->
                <ac:AdornedControl
                    HorizontalAdornerPlacement="Outside"
                    VerticalAdornerPlacement="Outside"
                    AdornerOffsetX="-12"
                    AdornerOffsetY="8"
                    IsMouseOverShowEnabled="{Binding ElementName=networkControl, Path=IsNotDragging}"
                    >
                    
                    <!-- The margin has been selected so that the selection rect nicely covers the entire node. -->
                    
                    <Grid
                        MinWidth="120"
                        Margin="10,6,10,6"
                        SizeChanged="Node_SizeChanged"
                        >

                        <!-- This rectangle is the main visual for the node. -->
                        
                        <Rectangle
                            Stroke="{StaticResource nodeBorderBrush}"
                            StrokeThickness="1.3"
                            RadiusX="4"
                            RadiusY="4"
                            Fill="{StaticResource nodeFillBrush}"
                            />

                        <!-- 
                        This grid contains the node's connectors.
                        The margin is negative so that the connectors overlap the body of the node and it's selection border.
                        -->
                        <Grid
                            Margin="-6,4,-6,4"
                            >
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto" />
                                <ColumnDefinition Width="*" MinWidth="10" />
                                <ColumnDefinition Width="Auto" />
                            </Grid.ColumnDefinitions>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="Auto" />
                                <!-- spacer -->
                                <RowDefinition Height="2" />                    
                                <RowDefinition Height="Auto" />
                            </Grid.RowDefinitions>

                            <!-- The name of the node. -->                
                            <TextBlock
                                Grid.Column="0"
                                Grid.ColumnSpan="3"
                                Grid.Row="0"
                                Text="{Binding Name}"
                                HorizontalAlignment="Center"
                                VerticalAlignment="Center"
                                />

                            <!-- Displays the node's input connectors. -->                
                            <ItemsControl
                                Grid.Column="0"
                                Grid.Row="2"
                                ItemsSource="{Binding InputConnectors}"
                                ItemTemplate="{StaticResource inputConnectorTemplate}"
                                Focusable="False"
                                />

                            <!-- Displays the node's output connectors. -->
                            <ItemsControl
                                Grid.Column="2"
                                Grid.Row="2"
                                ItemsSource="{Binding OutputConnectors}"
                                ItemTemplate="{StaticResource outputConnectorTemplate}"
                                Focusable="False"
                                />
                        </Grid>
                    </Grid>
                    
                    <ac:AdornedControl.AdornerContent>
                        
                        <!-- 
                        This is the adorner that pops up when the user hovers the mouse over the node.
                        It displays a button that the user can click to delete the node.
                        -->
                        <Canvas
                            x:Name="nodeAdornerCanvas"
                            HorizontalAlignment="Right"
                            VerticalAlignment="Top"
                            Width="30"
                            Height="30"
                            >
                            <Line
                                X1="0"
                                Y1="30"
                                X2="15"
                                Y2="15"
                                Stroke="Black"
                                StrokeThickness="1"
                                />
                            <Button
                                x:Name="deleteNodeButton"
                                Canvas.Left="10"
                                Canvas.Top="0"
                                Width="20"
                                Height="20"
                                Cursor="Hand"
                                Focusable="False"
                                Command="{StaticResource Commands.DeleteNode}"
                                CommandParameter="{Binding}"
                                Template="{StaticResource deleteButtonTemplate}"
                                />
                        </Canvas>                        
                    </ac:AdornedControl.AdornerContent>
                </ac:AdornedControl>                    
            </DataTemplate>            
            
        </ResourceDictionary>
        
    </Window.Resources>
     
    <Window.InputBindings>        
        
        <!-- Bind input to commands. -->
        
        <KeyBinding
			Key="Del"
			Command="{StaticResource Commands.DeleteSelectedNodes}"
			/>
        <KeyBinding
			Key="Minus"
			Command="{StaticResource Commands.ZoomOut}"
			/>
        <KeyBinding
			Key="Plus"
			Command="{StaticResource Commands.ZoomIn}"
			/>
        <KeyBinding
			Key="Backspace"
			Command="{StaticResource Commands.JumpBackToPrevZoom}"
			/>
        <KeyBinding
			Key="Space"
			Command="{StaticResource Commands.FitContent}"
			/>        
        
    </Window.InputBindings>
    
    <Window.CommandBindings>

        <!-- Bind commands to event handlers.-->
        
        <CommandBinding 
            Command="{StaticResource Commands.DeleteSelectedNodes}" 
            Executed="DeleteSelectedNodes_Executed" 
            />
        <CommandBinding 
            Command="{StaticResource Commands.CreateNode}" 
            Executed="CreateNode_Executed" 
            />
        <CommandBinding 
            Command="{StaticResource Commands.DeleteNode}" 
            Executed="DeleteNode_Executed" 
            />       
        <CommandBinding 
            Command="{StaticResource Commands.DeleteConnection}" 
            Executed="DeleteConnection_Executed" 
            />
        <CommandBinding 
            Command="{StaticResource Commands.ZoomOut}" 
            Executed="ZoomOut_Executed" 
            />
        <CommandBinding 
            Command="{StaticResource Commands.ZoomIn}" 
            Executed="ZoomIn_Executed" 
            />
        <CommandBinding 
            Command="{StaticResource Commands.JumpBackToPrevZoom}" 
            Executed="JumpBackToPrevZoom_Executed" 
            CanExecute="JumpBackToPrevZoom_CanExecuted"
            />
        <CommandBinding 
            Command="{StaticResource Commands.FitContent}" 
            Executed="FitContent_Executed" 
            />
        <CommandBinding 
            Command="{StaticResource Commands.Fill}" 
            Executed="Fill_Executed" 
            />
        <CommandBinding 
            Command="{StaticResource Commands.OneHundredPercent}" 
            Executed="OneHundredPercent_Executed" 
            />

    </Window.CommandBindings>

    <Window.ContextMenu>
        
        <ContextMenu>
            
            <!-- Clicking this menu item creates a new node. -->
            
            <MenuItem
                Header="Create Node"
                Command="{StaticResource Commands.CreateNode}"
                ToolTip="Creates a new node"
                />
            
            <Separator />

            <MenuItem
                Header="Fit"
                Command="{StaticResource Commands.FitContent}"
                ToolTip="Fit selected nodes to the viewport, when nothing is selected fits all nodes to the viewport"
                />
            <MenuItem
                Header="Fill"
                Command="{StaticResource Commands.Fill}"
                ToolTip="Fit the entire content area to the viewport"
                />
            <MenuItem
                Header="100%"
                Command="{StaticResource Commands.OneHundredPercent}"
                ToolTip="Scale the content to 100%"
                />
            <MenuItem
                Header="Previous Zoom"
                Command="{StaticResource Commands.JumpBackToPrevZoom}"
                ToolTip="Return to the previous zoom level"
                />

            <Separator />

            <MenuItem
                Header="Zoom Out"
                Command="{StaticResource Commands.ZoomOut}"
                ToolTip="Zooms out from the canvas"
                />
            <MenuItem
                    Header="Zoom In"
                    Command="{StaticResource Commands.ZoomOut}"
                    ToolTip="Zooms in on the canvas"
                    />

        </ContextMenu>
    </Window.ContextMenu>

    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <DockPanel>
        
        <!-- Grid placed below the zoom and pan control that contains the zoom slider, zoom label and some buttons. -->
            
        <Grid
            Margin="5,5,5,5"
            DockPanel.Dock="Bottom"
            >
            <Grid.ColumnDefinitions>
                
                <!-- empty space -->
                <ColumnDefinition Width="*" />
                
                <!-- the fit content button -->
                <ColumnDefinition Width="40" />

                <!-- spacer -->
                <ColumnDefinition Width="5" />

                <!-- the fill button -->
                <ColumnDefinition Width="40" />

                <!-- spacer -->
                <ColumnDefinition Width="5" />

                <!-- the 100% button -->
                <ColumnDefinition Width="40" />

                <!-- spacer -->
                <ColumnDefinition Width="5" />

                <!-- the zoom percentage label -->
                <ColumnDefinition Width="25" />

                <!-- the % symbol -->
                <ColumnDefinition Width="15" />

                <!-- spacer -->
                <ColumnDefinition Width="5" />

                <!-- the zoom out button -->
                <ColumnDefinition Width="20" />

                <!-- spacer -->
                <ColumnDefinition Width="5" />

                <!-- the zoom slider -->
                <ColumnDefinition Width="120" />

                <!-- spacer -->
                <ColumnDefinition Width="5" />

                <!-- the zoom in button -->
                <ColumnDefinition Width="20" />

                <!-- spacer -->
                <ColumnDefinition Width="10" />

                <!-- resize grip -->
                <ColumnDefinition Width="Auto" />
                
            </Grid.ColumnDefinitions>

            <!-- 
            The 'fit content' button.  Causes the content to be scaled so that all the graph nodes fit in the viewport.
            -->
            <Button
                Grid.Column="1"
                Command="{StaticResource Commands.FitContent}"
                ToolTip="Fit all nodes to the viewport"
                >
                Fit
            </Button>

            <!-- 
            The fill button.  Causes the content to be scaled so that it fits in the viewport.
            -->
            <Button
                Grid.Column="3"
                Command="{StaticResource Commands.Fill}"
                ToolTip="Fit the entire content area to the viewport"
                >
                Fill
            </Button>
            
            <!-- 
            The 100% button.  Causes the content to be scaled to 100 percent.
            -->
            <Button
                Grid.Column="5"
                Command="{StaticResource Commands.OneHundredPercent}"
                ToolTip="Scale the content to 100%"
                >
                100%
            </Button>

            <!--
            This is the label that shows what the current zoom level
            is while zooming in and out.
            -->
            <TextBlock
                Grid.Column="7"
                VerticalAlignment="Center"
                HorizontalAlignment="Right"
                Text="{Binding ElementName=zoomAndPanControl, Path=ContentScale, Converter={StaticResource scaleToPercentConverter}}"
                />
            
            <TextBlock
                Grid.Column="8"
                VerticalAlignment="Center"
                Text="%"
                />

            <!-- Button to zoom out. -->
            <Button
                Grid.Column="10"
                Command="{StaticResource Commands.ZoomOut}"
                ToolTip="Zoom out from the content"
                >
                -
            </Button>

            <!-- Slider to change the current zoom level. -->
            <Slider
                Grid.Column="12"
                Minimum="10" 
                LargeChange="20" 
                TickFrequency="10" 
                Maximum="200" 
                SmallChange="10" 
                TickPlacement="TopLeft"
                Value="{Binding ElementName=zoomAndPanControl, Path=ContentScale, Converter={StaticResource scaleToPercentConverter}}"
                ToolTip="Change the zoom level of the content"
                />

            <!--
            Button to zoom in.
            -->
            <Button
                Grid.Column="14"
                Command="{StaticResource Commands.ZoomIn}"
                ToolTip="Zoom in on the content"
                >
                +
            </Button>
            
            <!-- Stick a resize grip in the bottom right corner of the window. -->
            <ResizeGrip
                Grid.Column="16"
                />

        </Grid>

        <!-- 
        The ScrollViewer displays scrollbars when the content is too big to fit in the viewport. 
        Focusable is set to False because it is only that NetworkView that I want to be focusable.
        -->
        <ScrollViewer
            CanContentScroll="True"
            VerticalScrollBarVisibility="Visible"
            HorizontalScrollBarVisibility="Visible"
            Focusable="False"
            >
            
            <!-- 
            The ZoomAndPanControl displays the NetworkView as its content.
            We can use the mouse to zoom and pan about the NetworkView.
            -->
            <ZoomAndPan:ZoomAndPanControl
                x:Name="zoomAndPanControl"
                ContentScale="{Binding ContentScale, Mode=TwoWay}"
                ContentOffsetX="{Binding ContentOffsetX, Mode=TwoWay}"
                ContentOffsetY="{Binding ContentOffsetY, Mode=TwoWay}"
                ContentViewportWidth="{Binding ContentViewportWidth, Mode=OneWayToSource}"
                ContentViewportHeight="{Binding ContentViewportHeight, Mode=OneWayToSource}"
                Background="LightGray"
                Focusable="False"
                >
                <!--
                We need to nest our NetworkView in an AdornerDecorator so that adorners work correct when
                we zoom in and out using the ZoomAndPanControl
                -->
                <AdornerDecorator>                
                    <!--
                    This grid specifies the size of the ZoomAndPanControl's content.
                    It wraps up both the NetworkView and drag-zooming Canvas.
                    -->
                    <Grid
                        Width="{Binding ContentWidth}"
                        Height="{Binding ContentHeight}"
                        >

                        <!-- In this sample the NetworkView is the content displayed by the ZoomAndPanControl. -->
                        
                        <NetworkUI:NetworkView
                            x:Name="networkControl"
                            
                            NodesSource="{Binding Network.Nodes}"
                            ConnectionsSource="{Binding Path=Network.Connections}"
                            
                            ConnectionDragStarted="networkControl_ConnectionDragStarted"
                            QueryConnectionFeedback="networkControl_QueryConnectionFeedback"
                            ConnectionDragging="networkControl_ConnectionDragging"                            
                            ConnectionDragCompleted="networkControl_ConnectionDragCompleted"
                            
                            MouseDown="networkControl_MouseDown"
                            MouseUp="networkControl_MouseUp"
                            MouseMove="networkControl_MouseMove"
                            MouseWheel="networkControl_MouseWheel"
                            />
                        
                        <!--
                        This Canvas and Border are used as a very simple way to render a drag rectangle that the user
                        uses to specify an area to zoom in on.
                        -->
                        <Canvas
                            x:Name="dragZoomCanvas"
                            Visibility="Collapsed"
                            >
                            <Border 
                                x:Name="dragZoomBorder"
                                BorderBrush="Black"
                                BorderThickness="1"
                                Background="Silver"
                                CornerRadius="1"
                                Opacity="0"
                                />
                        </Canvas>
                        
                        
                        
                    </Grid>
                </AdornerDecorator>
            </ZoomAndPan:ZoomAndPanControl>
        </ScrollViewer>
    </DockPanel>
</Window>

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The MIT License


Written By
Chief Technology Officer
Australia Australia
Software craftsman | Author | Writing rapidfullstackdevelopment.com - Posting about how to survive and flourish as a software developer

Follow on Twitter for news and updates: https://twitter.com/codecapers

I'm writing a new book: Rapid Fullstack Development. Learn from my years of experience and become a better developer.

My second book, Bootstrapping Microservices, is a practical and project-based guide to building distributed applications with microservices.

My first book Data Wrangling with JavaScript is a comprehensive overview of working with data in JavaScript.

Data-Forge Notebook is my notebook-style application for data transformation, analysis and transformation in JavaScript.

I have a long history in software development with many years in apps, web apps, backends, serious games, simulations and VR. Making technology work for business is what I do: building bespoke software solutions that span multiple platforms.

I have years of experience managing development teams, preparing technical strategies and creation of software products. I can explain complicated technology to senior management. I have delivered cutting-edge products in fast-paced and high-pressure environments. I know how to focus and prioritize to get the important things done.

Author

- Rapid Fullstack Development
- Bootstrapping Microservices
- Data Wrangling with JavaScript

Creator of Market Wizard

- https://www.market-wizard.com.au/

Creator of Data-Forge and Data-Forge Notebook

- http://www.data-forge-js.com
- http://www.data-forge-notebook.com

Web

- www.codecapers.com.au

Open source

- https://github.com/ashleydavis
- https://github.com/data-forge
- https://github.com/data-forge-notebook


Skills

- Quickly building MVPs for startups
- Understanding how to get the most out of technology for business
- Developing technical strategies
- Management and coaching of teams & projects
- Microservices, devops, mobile and fullstack software development

Comments and Discussions