乐筑天下

搜索
欢迎各位开发者和用户入驻本平台 尊重版权,从我做起,拒绝盗版,拒绝倒卖 签到、发布资源、邀请好友注册,可以获得银币 请注意保管好自己的密码,避免账户资金被盗
查看: 333|回复: 14

F# - Use WPF dialog button to draw a line

[复制链接]

69

主题

875

帖子

15

银币

顶梁支柱

Rank: 50Rank: 50

铜币
1146
发表于 2015-5-19 19:25:23 | 显示全部楼层 |阅读模式
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
[ol]
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:g='clr-namespace:WpfDialogSample;assembly=WpfDialogSample'
        Title="Window1" Height="100" Width="200">

   
        [B]
   
[/ol]
The F# Code
[ol]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.Status  PromptStatus.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.Status  PromptStatus.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()
[/ol]
回复

使用道具 举报

15

主题

687

帖子

169

银币

中流砥柱

Rank: 25

铜币
582
发表于 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
[ol]
        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">
   
        
   
   
        [B]
                Command="{Binding DrawLineCommand}" CommandParameter="{Binding ElementName=mainWindow}"/>
   
[/ol]
Model
[ol]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()[/ol]
View
[ol]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)[/ol]
ViewModel
[ol]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))  [/ol]
回复

使用道具 举报

69

主题

875

帖子

15

银币

顶梁支柱

Rank: 50Rank: 50

铜币
1146
发表于 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
[ol]let win = XamlReader.Load(File.OpenRead("MainWindow.xaml")) :?> Window
AcAp.ShowModelessWindow(win)
[/ol]
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.
回复

使用道具 举报

15

主题

687

帖子

169

银币

中流砥柱

Rank: 25

铜币
582
发表于 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.
[ol]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))  [/ol]
回复

使用道具 举报

15

主题

687

帖子

169

银币

中流砥柱

Rank: 25

铜币
582
发表于 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
[ol]
        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}"/>
            
            
                [B]
                        Command="{Binding OkCommand}" CommandParameter="{Binding ElementName=mainWindow}"/>
                [B]
                        Command="{Binding CancelCommand}" CommandParameter="{Binding ElementName=mainWindow}"/>
            
        
   
[/ol]
Model
[ol]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[B]
            |> addEntity (new Line(pr1.Value, pr2.Value, Layer = layer)) |> ignore
            tr.Commit();[/ol]
View
[ol]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 ()[/ol]
ViewModel
[ol]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)

[/ol]
回复

使用道具 举报

69

主题

875

帖子

15

银币

顶梁支柱

Rank: 50Rank: 50

铜币
1146
发表于 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.
回复

使用道具 举报

24

主题

204

帖子

6

银币

后起之秀

Rank: 20Rank: 20Rank: 20Rank: 20

铜币
300
发表于 2015-5-26 08:21:42 | 显示全部楼层
[ol]
        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}"/>
            
            
                [B]
                        Command="{Binding OkCommand}" CommandParameter="{Binding ElementName=mainWindow}"/>
                [B]
            
        
   
[/ol]
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.
回复

使用道具 举报

15

主题

687

帖子

169

银币

中流砥柱

Rank: 25

铜币
582
发表于 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.
回复

使用道具 举报

3

主题

28

帖子

1

银币

初来乍到

Rank: 1

铜币
40
发表于 2015-5-26 22:10:59 | 显示全部楼层

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

    }
}
[/ol]
回复

使用道具 举报

15

主题

687

帖子

169

银币

中流砥柱

Rank: 25

铜币
582
发表于 2015-5-30 14:51:35 | 显示全部楼层

Nice WPF demo. Let me suggest to use the standard F# event here, Control.EventMSDN.
[ol]type ViewModelBase() =
    let propertyChangedEvent = Event()
    interface INotifyPropertyChanged with
        [] member x.PropertyChanged = propertyChangedEvent.Publish
    member x.OnPropertyChanged propertyName =
        propertyChangedEvent.Trigger(x, PropertyChangedEventArgs propertyName)[/ol]
F# supports automatically implemented propertiesMSDN.
[ol]type ViewModel() =
    inherit ViewModelBase()

    member val Layer = "0" with get,set[/ol]
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.
[ol]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

[)>]
()[/ol]
回复

使用道具 举报

发表回复

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

  • 微信公众平台

  • 扫描访问手机版

  • 点击图片下载手机App

QQ|关于我们|小黑屋|乐筑天下 繁体中文

GMT+8, 2024-11-22 01:59 , Processed in 0.309601 second(s), 72 queries .

© 2020-2024 乐筑天下

联系客服 关注微信 帮助中心 下载APP 返回顶部 返回列表