F# - Use WPF dialog button to draw a line
Hi All,this is a discussion on an F# version from this thread
I'm interested in the best way to do this as WPF and F# are not as advanced as one would like.
It's quite doable but takes a bit more of a lower level way of doing things.
Here's my quick interpretation to get the ball rolling, cheers. (Using Bricscad V15, change assembly ref's as required for AutoCAD)
The Xaml
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:g='clr-namespace:WpfDialogSample;assembly=WpfDialogSample'
Title="Window1" Height="100" Width="200">
The F# Code
namespace WpfDialogSample
open System
open System.IO
open System.Windows
open System.Windows.Controls
open System.Windows.Markup
open Teigha.Runtime
open Teigha.DatabaseServices
open Teigha.Geometry
open Bricscad.EditorInput
open Bricscad.ApplicationServices
type AcAp = Bricscad.ApplicationServices.Application
// this should be refactored out to another file
module CadWorker =
let DrawLine (window:Window) =
let doc = AcAp.DocumentManager.MdiActiveDocument;
let db = doc.Database;
let p1 = new PromptPointOptions("Specify base point :");
let rep1 = doc.Editor.GetPoint(p1);
let pt1 = rep1.Value;
if (rep1.StatusPromptStatus.Cancel) then
let mutable p2 = new PromptPointOptions("Next point :")
p2.BasePoint
p2.UseBasePoint
let rep2 = doc.Editor.GetPoint(p2)
let pt2 = rep2.Value
if (rep2.StatusPromptStatus.Cancel) then
use tr = db.TransactionManager.StartTransaction()
let mSpace = tr.GetObject(SymbolUtilityServices.GetBlockModelSpaceId(db), OpenMode.ForWrite) :?> BlockTableRecord
let mutable line = new Line(pt1, pt2);
line.ColorIndex
mSpace.AppendEntity(line) |> ignore
tr.AddNewlyCreatedDBObject(line, true)
tr.Commit()
// a bit of a hack to close the dialog when done...
window.Close()
type CommandMethods() =
[]
member this.test() =
// create the dialog by loading the xaml as a Window
let win = XamlReader.Load(File.OpenRead("Window1.xaml")) :?> Window
// Hook into xaml button element here
let btn = win.FindName("DrawLine") :?> Button
// Handle click event on 'DrawLine' button, we pass the window so we can close it when done (i.e. a hack)
do btn.Click.Add( fun _ -> CadWorker.DrawLine win)
win.Show()
Nice Mick !
Starting from your example, I tried to make it closer to the MVVM pattern.
The Xaml
The button is bound to a Command which parameter is set to the window so that the command can close the window
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModel="clr-namespace:ViewModel;assembly=AcadFsWpf"
Name="mainWindow"
Title="Draw Line"
WindowStartupLocation="CenterOwner" Height="100" Width="200">
Command="{Binding DrawLineCommand}" CommandParameter="{Binding ElementName=mainWindow}"/>
Model
namespace Model
open System.Windows
open Autodesk.AutoCAD.ApplicationServices
open Autodesk.AutoCAD.DatabaseServices
open Autodesk.AutoCAD.EditorInput
open Autodesk.AutoCAD.Geometry
open Autodesk.AutoCAD.Runtime
type AcAp = Autodesk.AutoCAD.ApplicationServices.Application
module CadWorker =
let drawLine (param : obj) =
let doc = AcAp.DocumentManager.MdiActiveDocument
let db = doc.Database
let ed = doc.Editor
Autodesk.AutoCAD.Internal.Utils.SetFocusToDwgView();
let ppr = ed.GetPoint("\nStart point: ")
if ppr.Status = PromptStatus.OK then
let p1 = ppr.Value
let opts = PromptPointOptions("\nEnd point: ", BasePoint = p1, UseBasePoint = true)
let ppr = ed.GetPoint(opts)
if ppr.Status = PromptStatus.OK then
use docLock = doc.LockDocument()
use tr = db.TransactionManager.StartTransaction()
let btr = tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite) :?> BlockTableRecord
let line = new Line(p1, ppr.Value)
btr.AppendEntity(line) |> ignore
tr.AddNewlyCreatedDBObject(line, true)
tr.Commit();
(param |> unbox).Close()
View
namespace View
open System
open System.IO
open System.Windows
open System.Windows.Markup
open System.Xaml
open Autodesk.AutoCAD.ApplicationServices
open Autodesk.AutoCAD.Runtime
type AcAp = Autodesk.AutoCAD.ApplicationServices.Application
type CommandMethods() =
[]
member x.Test () =
let win = XamlReader.Load(File.OpenRead("MainWindow.xaml")) :?> Window
AcAp.ShowModelessWindow(win)
ViewModel
namespace ViewModel
open System
open System.Windows.Input
open Model
type RelayCommand (canExecute:(obj -> bool), action:(obj -> unit)) =
let event = new DelegateEvent()
interface ICommand with
[]
member x.CanExecuteChanged = event.Publish
member x.CanExecute arg = canExecute(arg)
member x.Execute arg = action(arg)
type ViewModel() =
member x.DrawLineCommand =
new RelayCommand(
(fun _ -> true),
(fun p -> CadWorker.drawLine p)) Very nice!
I'll have to absorb that for a bit but it looks pretty straight forward.
I tried to use
let win = XamlReader.Load(File.OpenRead("MainWindow.xaml")) :?> Window
AcAp.ShowModelessWindow(win)
but got a cast error with the win object not being of type 'Form'. I'll re-check my references with yours, maybe it's a Bricscad thing(?).
Thanks for your input Gile. Attached a screenshot with my references.
The next step should be to add a base type in the ViewModel which implements INotifyPropertyChanged so that the bound properties can notify a changed property.
namespace ViewModel
open System
open System.Windows.Input
open System.ComponentModel
open Model
type ViewModelBase() =
let propertyChangedEvent = new DelegateEvent()
interface INotifyPropertyChanged with
[]
member x.PropertyChanged = propertyChangedEvent.Publish
member x.OnPropertyChanged propertyName =
propertyChangedEvent.Trigger([| x; new PropertyChangedEventArgs(propertyName) |])
type RelayCommand (canExecute:(obj -> bool), action:(obj -> unit)) =
let event = new DelegateEvent()
interface ICommand with
[]
member x.CanExecuteChanged = event.Publish
member x.CanExecute arg = canExecute(arg)
member x.Execute arg = action(arg)
type ViewModel() =
inherit ViewModelBase()
member x.DrawLineCommand =
new RelayCommand(
(fun _ -> true),
(fun p -> CadWorker.drawLine p)) Hi,
Mick, I really thank you.
You have motivated me to get back a little to F# that I did not have much time to learn these days.
As I also try to learn WPF, mixing the two is fun.
Here's my last attempt. I tried to go a little further with WPF binding features and include some F# helpers (thanks to kaefer for this).
This time the window is run as a modal dialog, closed using DialogResult.
The Xaml
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModel="clr-namespace:ViewModel;assembly=AcadFsWpf2013"
Name="mainWindow"
Title="Draw Line"
WindowStartupLocation="CenterOwner" Height="120" Width="280" ResizeMode="NoResize">
ItemsSource="{Binding Layers}" SelectedItem="{Binding Layer}"/>
Command="{Binding OkCommand}" CommandParameter="{Binding ElementName=mainWindow}"/>
Command="{Binding CancelCommand}" CommandParameter="{Binding ElementName=mainWindow}"/>
Model
namespace Model
open System
open Autodesk.AutoCAD.ApplicationServices
open Autodesk.AutoCAD.DatabaseServices
open Autodesk.AutoCAD.EditorInput
open Autodesk.AutoCAD.Geometry
open Autodesk.AutoCAD.Runtime
type AcAp = Autodesk.AutoCAD.ApplicationServices.Application
type AcEx = Autodesk.AutoCAD.Runtime.Exception
module Helpers =
let getObject DBObject> (id : ObjectId) =
id.GetObject(OpenMode.ForRead) :?> 'a
let getObjects DBObject>: System.Collections.IEnumerable -> _ =
let rxc = RXClass.GetClass(typeof)
Seq.cast
>> Seq.choose (function
| id when id.ObjectClass.IsDerivedFrom(rxc) -> Some(getObject id)
| _ -> None)
let addEntity (ent : #Entity) (btr : BlockTableRecord) =
if not btr.IsWriteEnabled then btr.UpgradeOpen()
let id = btr.AppendEntity(ent)
btr.Database.TransactionManager.AddNewlyCreatedDBObject(ent, true)
id
type OptionBuilder() =
member b.Bind(x, f) = Option.bind f x
member b.Return(x) = Some x
member b.Zero() = None
let opt = new OptionBuilder()
let failIfNotOk (pr : #PromptResult) =
opt { if pr.Status = PromptStatus.OK then return pr }
type Editor with
member ed.GetPoint(pt, msg) =
ed.GetPoint(new PromptPointOptions(msg, BasePoint = pt, UseBasePoint = true))
open Helpers
module CadWorker =
let getLayers () =
let db = HostApplicationServices.WorkingDatabase
use tr = db.TransactionManager.StartTransaction()
db.LayerTableId
|> getObject
|> getObjects
|> Seq.map (fun l -> l.Name)
|> Seq.toArray
let drawLine (layer) =
let doc = AcAp.DocumentManager.MdiActiveDocument
let db = doc.Database
let ed = doc.Editor
let result = opt {
let! pr1 = failIfNotOk (ed.GetPoint("\nStart point: "))
let! pr2 = failIfNotOk (ed.GetPoint(pr1.Value, "\nEnd point: "))
return (pr1, pr2) }
match result with
| None -> ()
| Some (pr1, pr2) ->
use tr = db.TransactionManager.StartTransaction()
db.CurrentSpaceId
|> getObject
|> addEntity (new Line(pr1.Value, pr2.Value, Layer = layer)) |> ignore
tr.Commit();
View
namespace View
open System
open System.IO
open System.Windows
open System.Windows.Markup
open System.Xaml
open Autodesk.AutoCAD.ApplicationServices
open Autodesk.AutoCAD.Runtime
type AcAp = Autodesk.AutoCAD.ApplicationServices.Application
type CommandMethods() =
[]
member x.Test () =
let win = XamlReader.Load(File.OpenRead("MainWindow.xaml")) :?> Window
AcAp.ShowModalWindow(win) |> ignore
[)>]
do ()
ViewModel
namespace ViewModel
open System
open System.ComponentModel
open System.Windows
open System.Windows.Input
type ViewModelBase() =
let propertyChangedEvent = new DelegateEvent()
interface INotifyPropertyChanged with
[]
member x.PropertyChanged = propertyChangedEvent.Publish
member x.OnPropertyChanged propertyName =
propertyChangedEvent.Trigger([| x; new PropertyChangedEventArgs(propertyName) |])
type RelayCommand (canExecute:(obj -> bool), action:(obj -> unit)) =
let event = new DelegateEvent()
interface ICommand with
[]
member x.CanExecuteChanged = event.Publish
member x.CanExecute arg = canExecute(arg)
member x.Execute arg = action(arg)
open Model.CadWorker
type ViewModel() =
inherit ViewModelBase()
let mutable layer = "0"
let resultOk param =
(unbox param).DialogResult
drawLine(layer)
let resultCancel param =
(unbox param).DialogResult
member x.Layer
with get () = layer
and set v = layer
member x.Layers
with get () = getLayers()
member x.OkCommand =
new RelayCommand((fun _ -> true), resultOk)
member x.CancelCommand =
new RelayCommand((fun _ -> true), resultCancel)
Thank You Gile,
There is not much out there for F# WPF or with AutoCAD so every bit helps!
I'll try to take a closer look later today, there's a bit to absorb and test with here, thanks.
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ViewModel="clr-namespace:ViewModel;assembly=AcadFsWpf2013"
Name="mainWindow"
Title="Draw Line"
WindowStartupLocation="CenterOwner" Height="120" Width="280" ResizeMode="NoResize">
ItemsSource="{Binding Layers}" SelectedItem="{Binding Layer}"/>
Command="{Binding OkCommand}" CommandParameter="{Binding ElementName=mainWindow}"/>
Some tweaks as you're learning WPF
1. Don't give names to Controls that you're not referencing somewhere else.You're just causing WPF to declare a bunch of variables in code behind that you're never using.
2. You can set your Cancel Button's IsCancel property to true and achieve the same result without creating a Command Binding. All bindings take overhead and until .NET 2015 are created at runtime taking more processor cycles.
Personally I set my OK button's click event in the code behind then you just set DialogResult to True and your done.In the spirit of MVVM the OK buttons action is part of the window's behavior and even if the model changes the OK button's behavior will always be the same. That's just my personal preference. Thank you very much for your advices MexicanCustard.
I do agree for the control's names and the IsCancel property.
In this case, about the OK button, I thaught it was a good thing to set the action in the ViewModel because it requires the selected layer which is set in the ViewModel too.
Another thing to keep in mind is that F# doesn't natively provides the same features as C# or VB to build WPF objects (neither WinForms) so we have to use some tricks. The one used here (nicely provided by MickD) is to add a xaml file to the F# project and load it with XamlReader.Load() method. This way doesn't provide any "*.xaml.fs" file for the code behind.
Anyway, thanks again, your knowledge is very helpfull.
I'm so MVVM this is my typical code behind file
using System;
namespace MVVM.Master
{
public partial class MVVMWpf : Window
{
}
}
Nice WPF demo. Let me suggest to use the standard F# event here, Control.EventMSDN.
type ViewModelBase() =
let propertyChangedEvent = Event()
interface INotifyPropertyChanged with
[] member x.PropertyChanged = propertyChangedEvent.Publish
member x.OnPropertyChanged propertyName =
propertyChangedEvent.Trigger(x, PropertyChangedEventArgs propertyName)
F# supports automatically implemented propertiesMSDN.
type ViewModel() =
inherit ViewModelBase()
member val Layer = "0" with get,set
We're missing partial classes in F# and therefore Code-Behind. I'd like to solicit opinions on the advisability of embedding xaml-files into the assembly, to be loaded as Referenced Assembly Resource FileMSDN.
type CommandMethods() =
[]
member x.Test () =
let uri = Uri @"pack://application:,,,/AcadFsWpf2013;component/MainWindow.xaml"
let info = Application.GetResourceStream uri
info.Stream
|> XamlReader.Load
|> unbox
|> AcAp.ShowModalWindow
|> ignore
[)>]
()
页:
[1]
2