Navs.Avalonia
The Navs.Avalonia library is a small wrapper over Navs that targets the Control
type in Avalonia Applications.
This means this router can be used with plain Avalonia code without XAML for "code-behind" or "code-first" applications.
This project is meant to be used with NXUI or similar libnraries that provide a code-first approach to building Avalonia applications. It might be possible though to write a wrapping user control in XAML that uses this router. as it doesn't depend on any specific Avalonia features.
That being said...
Usage
Using this library is very similar to using the base Navs library. The main difference is that the Navs.Avalonia
library provides a less generic versions of the API.
open NXUI
open NXUI.FSharp
open Navs
open Navs.Avalonia
let HomeComponent(): Control =
UserControl()
.content(
StackPanel()
.children(
TextBlock.text("Home")
// ... other controls ...
)
)
let AboutComponent(): Control =
UserControl()
.content(
StackPanel()
.children(
TextBlock.text("About")
// ... other controls ...
)
)
let routes = [
Route.define("home", "/home", fun _ -> HomeComponent())
Route.define("about", "/about", fun _ -> AboutComponent())
]
let router: IRouter<Control> = AvaloniaRouter(routes)
let app =
Window()
.content(
DockPanel()
.lastChildFill(true)
.children(
navbar().DockTop() // or any othe component there,
// use the router outlet
RouterOutlet().router(router)
)
)
From there you can use the router
to navigate between the different components and any other usages you have with the base Navs library.
The RouterOutlet
The RouterOutlet
is a control that will render the current component based on the current route. It also provides page transitions so you can have a smooth experience when navigating between pages.
The RouterOutlet control provides three main properties:
- RouterOutlet.Router - The router to use to render the current component.
- RouterOutlet.PageTransition - The transition to use when the router content changes.
- RouterOutlet.NoContent - The content to render when the router content is
None
which can be when the navigation fails or the route is not found.
The outlet is built to be used like any other Avalonia control so you should be able to do things like:
RouterOutlet()
.router(router)
.PageTransition(SlideInTransition())
or in case you're using XAML and are interested in this project
|
Please keep in mind that if you try to modify the Content
property of the RouterOutlet
it most certainly will break the outlet's functionality.
Adaptive Data
If you're going down the Observable route (which lends itslef quite well with Avalonia) you should totally check out FSharp.Control.Reactive which includes a lot of functionality to work with them.
But internally we use FSharp.Data.Adaptive and also have added a few handy extensions to the Navs.Avalonia
library to make it easier to work with Adaptive Data.
open FSharp.Data.Adaptive
open Navs.Avalonia
let HomeComponent _ _ : Control =
// create local state
let counter, setCounter = AVal.useState 0
UserControl()
.content(
StackPanel()
.children(
TextBlock()
.text(
// using F# Computation Expressions
adaptive {
let! counter = counter
let double = counter * 2
return $"Counter: %d{counter} and double: %d{double}"
}
|> AVal.toBinding
),
TextBlock()
.text(
// using the AVal.map function
counter
|> AVal.map(fun counter ->
let triple = counter * 3
$"Counter: %d{counter} and triple: %d{triple}"
)
|> AVal.toBinding
)
Button()
.content("Increment")
.OnClickHandler(fun _ _ ->
let currentValue = counter.getValue()
// set local state
setCounter (currentValue + 1))
)
)
let routes = [
Route.define("home", "/home", HomeComponent)
]
let router: IRouter<Control> = AvaloniaRouter(routes)
let app =
Window()
.content(router.Content |> AVal.toBinding, BindingMode.OneWay)
F# Adaptive is a reactive model that can be more efficient than standard observables, if you come from the web ecosystem then you might have heard about "signals" which is a very similar concept.
Granular and cacheable updates are the main features of Adaptive Data.
For shared state between components you can pass down the adaptive data and consume it in the child components.
let siblingA(value: aval<int>) =
TextBlock()
.text(
value
|> AVal.map(fun v -> $"Sibling A: %d{v}")
|> AVal.toBinding
)
let siblingB(value: aval<int>) =
TextBlock()
.text(
value
|> AVal.map(fun v -> $"Sibling B: %d{v}")
|> AVal.toBinding
)
let siblingC(value: cval<int>) =
StackPanel()
.OrientationHorizontal()
.spacing(10)
.children(
TextBlock()
.text(
value
|> AVal.map(fun v -> $"Sibling C: %d{v}")
|> AVal.toBinding
),
Button()
.content("Increment")
.OnClickHandler(fun _ _ ->
value.setValue(fun v -> v + 1)
)
)
let parent() =
let sharedValue = cval 0
StackPanel()
.spacing(4)
.children(
siblingA(sharedValue)
siblingB(sharedValue)
siblingC(sharedValue)
)
In the above example, siblingA
, siblingB
, and siblingC
are all consuming the same sharedValue
but the first two are using aval
which is basically a read-only interface. the siblingC
is using cval
which is a read-write interface.
And although this might look like "double-way binding" it's not, the cval
emits updates in a uni-directional way, the adaptive values depending on it will cascade updates rather than mutating the current in a "listener" fashion.
An alternative of course would be to hoist the updates to the parent and all of the sibling share a read-only interface.
let siblingC(value: aval<int>, onIncrement) =
StackPanel()
.OrientationHorizontal()
.spacing(10)
.children(
TextBlock()
.text(
value
|> AVal.map(fun v -> $"Sibling C: %d{v}")
|> AVal.toBinding
),
Button()
.content("Increment")
.OnClickHandler(fun _ _ -> onIncrement())
)
let parent() =
let sharedValue = cval 0
let onIncrement () =
sharedValue.setValue(fun v -> v + 1)
StackPanel()
.spacing(4)
.children(
siblingA(sharedValue)
siblingB(sharedValue)
siblingC(sharedValue, onIncrement)
)
Hoisting events is a common pattern and tends to be the most flexible way to handle updates, changeable values can act also like stores however, so they can be passed around without much issue as updates are always predictable and transactional.
For cases where you might want to bind the value to a control and make changes propagate automatically, you can use changeable values
which are adaptive values that can be set directly.
let myTextBox(value: cval<string>) =
TextBox()
.text(
value
|> CVal.toBinding
)
let parent() =
let sharedValue = cval "Hello"
*Note*: For double way binding we're using the
toBinding
function available in theCVal
module rather thanAVal.toBinding
which is read-only.
StackPanel()
.spacing(4)
.children(
myTextBox(sharedValue)
TextBlock()
.text(
sharedValue
|> AVal.map(fun v -> $"Shared Value: %s{v}")
|> AVal.toBinding
)
)
In that example, the TextBox
will automatically update the sharedValue
when the user types in it, and the TextBlock
will update when the sharedValue
changes.
This can be very useful for cases where you want to share information that could be updated from multiple components in a reactive and functional way.
module Route from Navs
--------------------
type Route = static member define: name: string * path: string * handler: (RouteContext -> INavigable<Control> -> Async<#Control>) -> RouteDefinition<Control> + 2 overloads
static member Route.define: name: string * path: string * handler: (RouteContext -> INavigable<Avalonia.Controls.Control> -> #Avalonia.Controls.Control) -> RouteDefinition<Avalonia.Controls.Control>
static member Route.define: name: string * path: string * handler: (RouteContext -> INavigable<Avalonia.Controls.Control> -> System.Threading.CancellationToken -> System.Threading.Tasks.Task<#Avalonia.Controls.Control>) -> RouteDefinition<Avalonia.Controls.Control>
static member Route.define: name: string * path: string * handler: (RouteContext -> INavigable<Avalonia.Controls.Control> -> Async<#Avalonia.Controls.Control>) -> RouteDefinition<Avalonia.Controls.Control>
--------------------
static member Route.define: name: string * path: string * [<InlineIfLambda>] handler: (RouteContext -> INavigable<'View> -> Async<'View>) -> RouteDefinition<'View>
static member Route.define: name: string * path: string * [<InlineIfLambda>] handler: (RouteContext -> INavigable<'View> -> System.Threading.CancellationToken -> System.Threading.Tasks.Task<'View>) -> RouteDefinition<'View>
static member Route.define: name: string * path: string * [<InlineIfLambda>] handler: (RouteContext -> INavigable<'View> -> 'View) -> RouteDefinition<'View>
type AvaloniaRouter = interface IRouter<Control> new: routes: RouteDefinition<Control> seq * [<Optional>] ?splash: Func<Control> -> AvaloniaRouter
<summary> A router that is specialized to work with Avalonia types. This router will render any object that inherits from Avalonia's of Control. </summary>
--------------------
new: routes: RouteDefinition<Avalonia.Controls.Control> seq * [<System.Runtime.InteropServices.Optional>] ?splash: System.Func<Avalonia.Controls.Control> -> AvaloniaRouter
type RouterOutlet = inherit UserControl new: unit -> RouterOutlet member NoContent: Control with get, set member PageTransition: IPageTransition with get, set member Router: IRouter<Control> with get, set static member NoContentProperty: DirectProperty<RouterOutlet,Control> static member PageTransitionProperty: DirectProperty<RouterOutlet,IPageTransition> static member RouterProperty: DirectProperty<RouterOutlet,IRouter<Control>>
--------------------
new: unit -> RouterOutlet
namespace FSharp
--------------------
namespace Microsoft.FSharp
namespace FSharp.Data
--------------------
namespace Microsoft.FSharp.Data
<summary> Provide a friendly interface to handle local state via Adaptive data </summary>
val double: value: 'T -> double (requires member op_Explicit)
--------------------
type double = System.Double
--------------------
type double<'Measure> = float<'Measure>
<summary> Convert Adaptive data into a binding that can be handled by avalonia </summary>
val int: value: 'T -> int (requires member op_Explicit)
--------------------
type int = int32
--------------------
type int<'Measure> = int
val string: value: 'T -> string
--------------------
type string = System.String