Microsoft MVP성태의 닷넷 이야기
.NET Framework: 558. WPF - ICommand 동작 방식 [링크 복사], [링크+제목 복사]
조회: 32484
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 1개 있습니다.)

WPF - ICommand 동작 방식

그러고 보니, 제 블로그에서 ICommand를 다룬 적이 한번도 없군요. ^^;

이에 대해서는, 일단 다음의 글이 시작점으로 좋습니다.

ICommand is like a chocolate cake
; http://blogs.msdn.com/b/mikehillberg/archive/2009/03/20/icommand-is-like-a-chocolate-cake.aspx

정리해 보면, ICommand는 3개의 멤버를 포함하는 간단한 인터페이스입니다.

public interface ICommand
{
    void Execute(object parameter);
    bool CanExecute(object parameter);
    event EventHandler CanExecuteChanged;
}

인터페이스니까, ICommand가 의미가 있으려면 호출하는 측과 호출당하는 측 모두 ICommand에 대한 계약을 완료해야 합니다.

우선, 호출당하는 측은 ICommand를 구현한 클래스를 정의하고 이에 대한 인스턴스를 호출하는 측으로 전달해야 합니다. 대충 다음과 같이 구현할 수 있습니다.

using System;
using System.Windows.Input;

class Program
{
    static void Main(string[] args)
    {
        RelayCommand cmd = new RelayCommand();
    }
}

public class RelayCommand : ICommand
{
    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        Console.WriteLine(DateTime.Now + ": RelayCommand.Execute - " + parameter);
    }
}

그럼, 호출하는 측과 함께 ICommand 인터페이스를 이용해 서로 연동하는 부분도 구현해 보겠습니다.

using System;
using System.Windows.Input;

class Program
{
    static void Main(string[] args)
    {
        RelayCommand cmd = new RelayCommand();

        ConsolePrompt prompt = new ConsolePrompt();
        prompt.Command = cmd;
        prompt.Run();
    }
}

public class ConsolePrompt
{
    public ICommand Command
    {
        get; set;
    }

    public void Run()
    {
        while (true)
        {
            string txt = Console.ReadLine();
            if (string.IsNullOrEmpty(txt) == true)
            {
                break;
            }

            Command?.Execute(txt);
        }
    }
}

간단하죠? ^^ 저런 식으로 ICommand 인터페이스가 서로 다른 객체 간에 명령 전달 및 처리를 구현할 수 있습니다. ICommand의 구현 의미는, 위에서 보는 바와 같이 ConsolePrompt로부터 "실행해야 할 코드"를 외부로 분리했다는 정도입니다. 물론, 기존에도 delegate를 전달하거나 event를 정의함으로써 유사한 구현을 했습니다. 단지, ICommand는 이것을 인터페이스로 체계화했을 뿐입니다. 따라서, ICommand를 사용하는 WPF 계열 응용 프로그램을 만드는 경우가 아니라면 사실 알 필요는 거의 없습니다.




위에서 구현하지 않은 ICommand.CanExecute에 좀 더 살을 붙여볼까요? 가령 하루 중 오전 9시 ~ 10시 사이에는 명령어 처리를 할 수 없다고 가정해 다음과 같이 코딩할 수 있습니다.

public class ConsolePrompt
{
    // ... [생략] ...

    public void Run()
    {
        while (true)
        {
            string txt = Console.ReadLine();
            if (string.IsNullOrEmpty(txt) == true)
            {
                break;
            }

            if (Command?.CanExecute(null) == true)
            {
                Command?.Execute(txt);
            }
        }
    }
}

public class RelayCommand : ICommand
{
    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter)
    {
        return DateTime.Now.Hour != 9;
    }

    public void Execute(object parameter)
    {
        Console.WriteLine(DateTime.Now + ": RelayCommand.Execute - " + parameter);
    }
}

즉, 호출하는 측에서도 CanExecute를 호출해 상황 파악을 하는 배려가 있어야 상호 동작이 의도한대로 잘 되는 것입니다.

그렇다면 CanExecuteChanged는 언제 사용할까요?

위에서 ConsolePrompt는 명령을 실행하기 전 매번 CanExecute를 호출해 명령 처리가 가능한지를 조회해야 했는데요. 이를 CanExecute를 호출해야 할 필요가 있을 때에만, 즉 객체의 내부 환경에서 Command 실행에 영향을 주는 상태가 변화한 딱 그 시점에만 호출하도록 할 수 있습니다.

콘솔 예제라서 다소 억지스럽지만 타이머를 이용해 8시에서 9시로, 9시에서 10시로 변경하는 그 시점에만 ConsolePrompt가 CanExecute로 조회하도록 다음과 같이 바꿀 수 있습니다.

public class ConsolePrompt
{
    public ICommand Command
    {
        get; set;
    }

    public void Run()
    {
        _commandEnabled = Command?.CanExecute(null);
        Command.CanExecuteChanged += Command_CanExecuteChanged;

        while (true)
        {
            string txt = Console.ReadLine();
            if (string.IsNullOrEmpty(txt) == true)
            {
                break;
            }

            if (_commandEnabled == true)
            {
                Command?.Execute(txt);
            }
        }
    }

    bool _commandEnabled = false;

    private void Command_CanExecuteChanged(object sender, EventArgs e)
    {
        _commandEnabled = Command?.CanExecute(null);
    }
}

public class RelayCommand : ICommand
{
    public event EventHandler CanExecuteChanged;

    public RelayCommand()
    {
        Thread t = new Thread((e) =>
        {
            RelayCommand cmd = e as RelayCommand;
            int old = DateTime.Now.Hour;

            while (true)
            {
                int current = DateTime.Now.Hour;

                if ((old == 8 && current == 9) || (old == 9 && current == 10))
                {
                    cmd.CanExecuteChanged(cmd, EventArgs.Empty); // 명령어를 처리할 수 있는지에 대한 상태 변화를 알림.
                }

                old = current;

                Thread.Sleep(1000);
            }
        });

        t.IsBackground = true;
        t.Start(this);
    }

    public bool CanExecute(object parameter)
    {
        return DateTime.Now.Hour != 9;
    }

    public void Execute(object parameter)
    {
        Console.WriteLine(DateTime.Now + ": RelayCommand.Execute - " + parameter);
    }
}

위의 코드는 CanExecuteChanged 구현 사례를 보이기 위해 다소 억지스러운 상황을 가정한 것이지만, 어쨌든 기능은 대충 저렇게 한다고 보시면 됩니다.




이제 WPF로 이야기를 넘어갈까요?

위의 예에서 ICommand 인터페이스를 내부적으로 지원하는 ConsolePrompt와 같은 클래스들이 바로 WPF에서 제공해 주는 각종 컨트롤들입니다. 대표적으로 ButtonBase를 상속받은 것들이 이에 해당하는데, 이들은 모두 Command 속성을 구현하게 됩니다.

ButtonBase.Command Property
; https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.primitives.buttonbase.command

Button 객체들의 Command 속성은 내부적으로 ConsolePrompt가 구현했던 것과 유사한 방식으로 동작합니다.

일례로, 다음과 같이 Command 속성에 RelayCommand 객체를 할당해 줄 수 있습니다.

// MainWindow.xaml

<Window x:Class="WpfApplication1.MainWindow"
        ...[생략]...
        xmlns:local="clr-namespace:WpfApplication1"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Button Command="{Binding ClickCommand}" Content="Click" />
    </Grid>
</Window>

// MainWindow.xaml.cs

using System;
using System.Windows;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        ICommand _clickCommand = new RelayCommand();
        public ICommand ClickCommand
        {
            get { return _clickCommand; }
        }

        public MainWindow()
        {
            InitializeComponent();
        }
    }

    public class RelayCommand : ICommand
    {
        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            return true;
        }

        public void Execute(object parameter)
        {
            MessageBox.Show("RelayCommand.Execute called!");
        }
    }
}

UI를 가진 Button의 성격상, 이전에 살펴본 ConsolePrompt 객체보다 ICommand를 더 잘 활용합니다. 일례로 CanExecute 메서드에서 false를 반환하도록 하면,

public class RelayCommand : ICommand
{
    // ...[생략]...

    public bool CanExecute(object parameter)
    {
        return false;
    }

    // ...[생략]...
}

Button은 자신의 상태를 비활성화시켜버려 사용자가 누르지 못하게 만듭니다. 그런데, 버튼은 CanExecute를 최초 한번만 실행시켜 상태를 알아본 후 그 다음부터는 호출하지 않도록 되어 있습니다. 대신 CanExecuteChanged 이벤트를 구독하고 있기 때문에 상태가 바뀌는 경우 명시적으로 CanExecuteChanged 이벤트를 통해 알려야 합니다. 예를 들어, 화면에 텍스트 상자가 있어 그 안에 사용자가 텍스트를 입력한 경우에만 Button을 클릭 가능하도록 만들고 싶다면 다음과 같이 할 수 있습니다.

<Window x:Class="WpfApplication1.MainWindow"
        ...[생략]...
        xmlns:local="clr-namespace:WpfApplication1"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="25" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBox Name="_txtBox" />
        <Button Grid.Row="1" Command="{Binding ClickCommand}" Content="Click" />
    </Grid>
</Window>

using System;
using System.Windows;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        ICommand _clickCommand = new RelayCommand();
        public ICommand ClickCommand
        {
            get { return _clickCommand; }
        }

        public MainWindow()
        {
            InitializeComponent();

            _txtBox.TextChanged += _txtBox_TextChanged;
        }

        private void _txtBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
        {
            (_clickCommand as RelayCommand).FireChanged(_txtBox.Text.Length != 0);
        }
    }

    public class RelayCommand : ICommand
    {
        bool _canExceute = false;

        public event EventHandler CanExecuteChanged;

        public bool CanExecute(object parameter)
        {
            return _canExceute;
        }

        public void Execute(object parameter)
        {
            MessageBox.Show("RelayCommand.Execute called!");
        }

        public void FireChanged(bool cond)
        {
            _canExceute = cond; 
            CanExecuteChanged(this, EventArgs.Empty);
        }
    }
}

그런데, 상태가 바뀔 때마다 일일이 저런 식으로 알림 이벤트를 호출하는 것이 여간 귀찮은 작업이 아닐 수 없습니다. 그래서 WPF 수준에서 의존 속성들에 대한 상태 변경을 스스로 알 수 있기 때문에 그런 경우 발생하는 알림 이벤트를 별도로 정의해 두었는데 그것이 바로 CommandManager.RequerySuggested 이벤트입니다.

CommandManager.RequerySuggested Event
; https://learn.microsoft.com/en-us/dotnet/api/system.windows.input.commandmanager.requerysuggested

이를 이용하면 RelayCommand를 좀 더 간단하게 구성할 수 있습니다.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        ICommand _clickCommand;
        public ICommand ClickCommand
        {
            get
            {
                if (_clickCommand == null)
                {
                    _clickCommand = new RelayCommand(this._txtBox);
                }

                return _clickCommand;
            }
        }

        public MainWindow()
        {
            
            InitializeComponent();
        }
    }

    public class RelayCommand : ICommand
    {
        TextBox _target;

        public RelayCommand(TextBox txtBox)
        {
            _target = txtBox;
        }

        public event EventHandler CanExecuteChanged
        {
            add
            {
                CommandManager.RequerySuggested += value;
            }

            remove
            {
                CommandManager.RequerySuggested -= value;
            }
        }

        public bool CanExecute(object parameter)
        {
            return string.IsNullOrEmpty(_target.Text) == false;
        }

        public void Execute(object parameter)
        {
            MessageBox.Show("RelayCommand.Execute called!");
        }
    }
}

Button 객체는 Command 속성에 ICommand 상속 객체가 할당되면 그 인스턴스의 CanExecuteChanged 이벤트를 구독합니다. 따라서, 위의 코드에서는 Button 객체가 CanExecuteChanged.add를 호출하게 되고 Button 측에서 전달한 이벤트 핸들러 메서드를 가리키는 value는 다시 CommandManager.RequerySuggested 이벤트로 전달하게 되므로 자연스럽게 WPF 내부에서 RequerySuggested 이벤트를 발생시킬 때마다 버튼은 CanExecuteChanged 알림을 받게 되고 이어서 CanExecute를 호출해 객체의 상태를 조회하게 됩니다.




그렇다면 WPF의 RoutedCommand는 도대체 뭘까요?

이 글에서 우리는 ICommand 객체를 구현한 RelayCommand를 사용하고 있는데요. 마찬가지로 WPF는 스스로 사용할 목적으로 RoutedCommand를 구현해 놓은 것 뿐입니다. 즉, ICommand의 구현체 중 하나에 속합니다.

그럼, RoutedUICommand는 또 뭘까요? 이에 대해서는 다음의 글에 잘 설명되어 있습니다.

WPF - RoutedCommand vs.RoutedUICommand 
; http://compilewith.net/2008/03/wpf-routedcommand-vsrouteduicommand.html

RoutedUICommand는 RoutedCommand를 상속받은 것으로 역시 WPF가 구현해 놓고 있습니다. 그리고 그 차이점은 Text 속성을 갖는다는 것 뿐입니다. 이것이 언제 쓰이냐면??? 바로 MenuItem같은 HeaderedItemsControl을 상속받은 객체들이 Command에 RoutedUICommand를 설정하면 그 MenuItem의 Header값으로 RoutedUICommand.Text 값을 사용하게 됩니다.

간단한 예를 만들어 보면!

<Window x:Class="WpfApplication1.MainWindow"
        ...[생략]...
        xmlns:local="clr-namespace:WpfApplication1"
        DataContext="{Binding RelativeSource={RelativeSource Self}}"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Menu>
            <Menu.CommandBindings>
                <CommandBinding Command="local:MainWindow.CopyCommand" 
                            Executed="CopyCommand_Executed" CanExecute="CopyCommand_CanExecute" />
            </Menu.CommandBindings>
            
            <MenuItem Header="Menu1">
                <MenuItem Command="local:MainWindow.CopyCommand" />
            </MenuItem>
        </Menu>
    </Grid>
</Window>

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        public static readonly RoutedUICommand CopyCommand =
                new RoutedUICommand("MyCopy", "CopyFunc", typeof(MainWindow), null);

        public MainWindow()
        {
            InitializeComponent();
        }

        private void CopyCommand_CanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            e.CanExecute = true;
        }

        private void CopyCommand_Executed(object sender, ExecutedRoutedEventArgs e)
        {
            MessageBox.Show("RoutedUICommand.Execute called!");
        }
    }
}

보는 바와 같이 <MenuItem Command="local:MainWindow.CopyCommand" /> 에는 Header 속성이 정의되어 있지 않지만, 그것의 Command로 할당한 RoutedUICommand 객체의 생성자에 전달된 "MyCopy"라는 문자열이 있기 때문에 메뉴가 보여지는 경우 "MyCopy"로 보이게 됩니다. 물론, <MenuItem Header="MyCopy2" Command="local:MainWindow.CopyCommand" />라고 명시적인 Header 값을 부여하는 것도 가능합니다. 이런 경우 RoutedUICommand에 설정된 값을 무시합니다.



ICommand가 뜨는 이유는, 이것을 활용하는 경우 멋있는 MVVM(Model-View-ViewModel) 구조의 응용 프로그램 작성이 가능하기 때문입니다. 이에 대해서는 이야기가 또 한가득이니 일단 접고.

문제는, 기존 컨트롤들이 정의한 이벤트들이 모두 ICommand로 제공하지는 않는다는 점입니다. 일례로, Window의 크기가 변경되는 SizeChanged 이벤트에 대한 ICommand 속성은 없습니다. 애당초 SizeChangedCommand라는 속성으로 이벤트 대신 ICommand용 속성이 제공된다면 좋을 텐데 모든 이벤트를 그렇게 중복 정의해 놓는 것도 그리 바람직하지는 않습니다. 게다가, 이벤트라는 것은 '알림'에 가까운 특징이 있는 반면 ICommand는 말 그대로 '명령 실행'이라는 점에서 그 구현 배경이 달라지는 점도 있습니다.

하지만, MVVM의 ViewModel을 이용해 UI와 코드의 분리를 확실하게 이루기 위해서는 기존 이벤트들에 대한 ICommand로의 처리 방안이 필요합니다. 이 때문에 나온 것들이 바로 다양한 MVVM 프레임워크에서 제공해주는 EventToCommand 같은 구현체들입니다.

MVVM - Commands, RelayCommands and EventToCommand
; https://learn.microsoft.com/en-us/archive/msdn-magazine/2013/may/mvvm-commands-relaycommands-and-eventtocommand

이에 대한 고민을 좀 더 확실히 느끼고 싶은 분들은 다음의 4부작 글을 추천해드립니다. ^^

View의 Event를 ViewModel에서 핸들링하기 - 개요  WPF & C# 
; http://blog.naver.com/vactorman/220516516289

View의 Event를 ViewModel에서 핸들링하기 - EventTrigger
; http://blog.naver.com/vactorman/220516524223

View의 Event를 ViewModel에서 핸들링하기 - ACB
; http://blog.naver.com/vactorman/220516529243

View의 Event를 ViewModel에서 핸들링하기 - Markup
; http://blog.naver.com/vactorman/220516534284

EventToCommand를 이용한 ViewModel 이벤트 핸들링
; http://blog.naver.com/vactorman/221064433289

이 정도면 웬만큼 정리된 것 같군요. ^^

(첨부한 파일은 이 글의 예제 코드를 포함합니다.)




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/9/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2017-08-07 05시28분
[Denial] 훌륭한 정리네요. 감사합니다.
[guest]
2022-03-04 11시19분
[김기헌] 감사합니다 선생님 ICommand 관련 여러 레퍼런스를 찾아봤지만 이해하기가 제일 쉬웠습니다
[guest]
2022-09-05 12시39분
[한예지] 쉽게 알려주셔서 정말 감사드립니다!
[guest]
2022-09-05 08시26분
[한혜진] 선생님 결국 아래 줄에서 old = current; 실행하는데
old = 10 을 넣은 이유가 궁금합니다.

if ((old == 8 && current == 9) || (old == 9 && current == 10))
{
     old = 10; // ← ★ 이 코드를 넣은 의도가 궁금합니다...
     cmd.CanExecuteChanged(cmd, EventArgs.Empty);
}
old = current;
[guest]
2022-09-05 09시18분
가끔 이거저거 하다가 테스트 코드가 함께 복붙되기도 합니다. (의미 없는 코드입니다. ^^; 본문의 코드는 수정했습니다.)
정성태
2022-09-07 02시10분
[한예지] [질문 ①] 선생님 이벤트가 발생하면 Main 스레드는 현재 실행하고 있던 코드의 위치(X)를 스택 프레임에 저장하고

Main 스레드는 이벤트 핸들러 안에 있는 코드들을 모두 실행한 후,

스택 프레임에 저장했던 위치(X)로 다시 복귀한 후, X를 아래 있는 코드들을 실행한다고 생각하는 것이 맞나요?

[질문 ②] 질문 ①이 맞다면 아래 코드 해석도 정확하겠죠?
디버깅을 통해서 확인하기는 했는데 혹시 순서만 맞고 내용이 틀렸을 수도 있어서 질문드립니다...
class Program
{
    static void Main(string[] args)
    {
        RelayCommand cmd = new RelayCommand();
        ConsolePrompt prompt = new ConsolePrompt();
        prompt.Command = cmd;
        prompt.Run();
    }
}

public class ConsolePrompt
{
    public ICommand Command
    {
        get; set;
    }

    bool _commandEnabled = false;

    public void Run()
    {
        _commandEnabled = Command.CanExecute(null);
        Command.CanExecuteChanged += Command_CanExecuteChanged;

        while (true)
        {
            string txt = Console.ReadLine();         // ★★★
            if (string.IsNullOrEmpty(txt) == true)    
            {
                break;
            }

            if (_commandEnabled == true)            
            {
                Command.Execute(txt);
            }
        }
    }

    

    private void Command_CanExecuteChanged(object sender, EventArgs e)
    {
        Console.WriteLine("순서 ②"); // ●●
        _commandEnabled = Command.CanExecute(null);
        Console.WriteLine("순서 ④"); // ●●
    }
}


public class RelayCommand : ICommand
{
    public event EventHandler CanExecuteChanged;

    public RelayCommand()
    {
        Thread t = new Thread((e) =>
        {
            RelayCommand cmd = e as RelayCommand;
            int old = DateTime.Now.Hour;

            while (true)
            {
                int current = DateTime.Now.Hour;

                if ((old == 8 && current == 9) || (old == 9 && current == 10))
                {
                    Console.WriteLine("순서 ①"); // ●●
                    cmd.CanExecuteChanged(cmd, EventArgs.Empty);
                }

                old = current;

                Thread.Sleep(1000);
            }
        });

        t.IsBackground = true;
        t.Start(this);
    }

    public bool CanExecute(object parameter)
    {
        Console.WriteLine("순서 ③"); // ●●
        return DateTime.Now.Hour != 9;
    }

    public void Execute(object parameter)
    {
        Console.WriteLine(DateTime.Now + ": RelayCommand.Execute - " + parameter);
    }
}
① Main Thread가 ★★★가 표시된 부분에서 대기 중이다.
② RelayCommand 클래스 안에 있는 반복문에서 CanExecuteChanged 이벤트가 발생했다.
③ Main Thread는 스택 프레임에 ★★★가 표시된 위치를 저장한 후,
  ●●가 표시된 부분을 ①번부터 ④번까지 순서대로 실행시킨다.
⑤ Main Thread는 ①번부터 ④번까지 순서대로 실행 완료 후,
  스택 프레임에 저장된 ★★★가 표시된 위치로 복귀한 후, 사용자가 값을 입력했다면
  ★★★ 아래 코드들을 실행한다.
[guest]
2022-09-07 02시22분
해당 예제에서 스레는 2개입니다.

Main을 실행한 스레드는 Console.ReadLine에서 걸려 있고, 사용자가 입력 후 엔터 키를 친 경우에만 그 아래의 코드들을 실행할 뿐입니다.

언급하신 "●●" 코드는 모두 RelayCommand의 생성자에서 만든 스레드에서 실행됩니다.
정성태
2022-09-07 03시10분
[한예지] 좋은 글, 좋은 답변 정말 감사드립니다!
오후 03시 09분에 카뱅으로 커피 보내드렸습니다^^
다시 한 번 감사드립니다~!
[guest]
2022-09-07 03시17분
감사합니다. 카페인의 힘을 빌려 오후에도 열심히 코딩해 보겠습니다. ^^
정성태

1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13396정성태7/22/20233332스크립트: 54. 파이썬 pystack 소개 - 메모리 덤프로부터 콜 스택 열거
13395정성태7/20/20233306개발 환경 구성: 685. 로컬에서 개발 중인 ASP.NET Core/5+ 웹 사이트에 대해 localhost 이외의 호스트 이름으로 접근하는 방법
13394정성태7/16/20233250오류 유형: 873. Oracle.ManagedDataAccess.Client - 쿼리 수행 시 System.InvalidOperationException
13393정성태7/16/20233416닷넷: 2133. C# - Oracle 데이터베이스의 Sleep 쿼리 실행하는 방법
13392정성태7/16/20233278오류 유형: 872. Oracle - ORA-01031: insufficient privileges
13391정성태7/14/20233366닷넷: 2132. C# - sealed 클래스의 메서드를 callback 호출했을 때 인라인 처리가 될까요?
13390정성태7/12/20233338스크립트: 53. 파이썬 - localhost 호출 시의 hang 현상
13389정성태7/5/20233320개발 환경 구성: 684. IIS Express로 호스팅하는 웹을 WSL 환경에서 접근하는 방법
13388정성태7/3/20233505오류 유형: 871. 윈도우 탐색기에서 열리지 않는 zip 파일 - The Compressed (zipped) Folder '[...].zip' is invalid. [1]파일 다운로드1
13387정성태6/28/20233531오류 유형: 870. _mysql - Commands out of sync; you can't run this command now
13386정성태6/27/20233600Linux: 61. docker - 원격 제어를 위한 TCP 바인딩 추가
13385정성태6/27/20233799Linux: 60. Linux - 외부에서의 접속을 허용하기 위한 TCP 포트 여는 방법
13384정성태6/26/20233557.NET Framework: 2131. C# - Source Generator로 해결하는 enum 박싱 문제파일 다운로드1
13383정성태6/26/20233308개발 환경 구성: 683. GPU 런타임을 사용하는 Colab 노트북 설정
13382정성태6/25/20233351.NET Framework: 2130. C# - Win32 API를 이용한 윈도우 계정 정보 (예: 마지막 로그온 시간)파일 다운로드1
13381정성태6/25/20233730오류 유형: 869. Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
13380정성태6/24/20233193스크립트: 52. 파이썬 3.x에서의 동적 함수 추가
13379정성태6/23/20233206스크립트: 51. 파이썬 2.x에서의 동적 함수 추가
13378정성태6/22/20233093오류 유형: 868. docker - build 시 "CANCELED ..." 뜨는 문제
13377정성태6/22/20236861오류 유형: 867. 파이썬 mysqlclient 2.2.x 설치 시 "Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually" 오류
13376정성태6/21/20233282.NET Framework: 2129. C# - Polly를 이용한 클라이언트 측의 요청 재시도파일 다운로드1
13375정성태6/20/20232977스크립트: 50. Transformers (신경망 언어모델 라이브러리) 강좌 - 2장 코드 실행 결과
13374정성태6/20/20233106오류 유형: 866. 파이썬 - <class 'AttributeError'> module 'flask.json' has no attribute 'JSONEncoder'
13373정성태6/19/20234395오류 유형: 865. 파이썬 - pymssql 설치 관련 오류 정리
13372정성태6/15/20233103개발 환경 구성: 682. SQL Server TLS 통신을 위해 사용되는 키 길이 확인 방법
13371정성태6/15/20233111개발 환경 구성: 681. openssl - 인증서 버전(V1 / V3)
1  2  3  4  5  6  7  8  [9]  10  11  12  13  14  15  ...