MickD 发表于 2015-5-19 19:25:23

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()

gile 发表于 2015-5-21 02:58:01

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))

MickD 发表于 2015-5-21 03:08:36

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.

gile 发表于 2015-5-21 03:34:01

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))

gile 发表于 2015-5-22 17:36:17

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)

MickD 发表于 2015-5-22 19:32:17

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.

MexicanCustard 发表于 2015-5-26 08:21:42


      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.

gile 发表于 2015-5-26 15:19:22

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.

kaefer 发表于 2015-5-26 22:10:59


I'm so MVVM this is my typical code behind file
using System;
namespace MVVM.Master
{
    public partial class MVVMWpf : Window
    {

    }
}

gile 发表于 2015-5-30 14:51:35


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
查看完整版本: F# - Use WPF dialog button to draw a line