2018/01/05

C# 이벤트 발생

이벤트를 발생하는 방법.

속성의 값이 변경되면 이벤트를 발생하고 처리를 해보자.

event 키워드를 사용하여 이벤트를 선언 하고
이벤트 대리자를(핸들러 메서드 형식) 지정 한다.

public event EventHandler ValueChanged;

대리자는 반환값이 없고(void) 이벤트 발생 객체 와 이벤트 데이터를 인자로 한다.

public delegate void EventHandler(object sender, EventArgs e);

이 형식을 무시해도 되지만 MS 가이드를 따르도록 한다.
이벤트 핸들러가 void가 아니면 의미도 이상하다.

다음은 이벤트 선언과 발생의 코드 이다.

class Item
{
    public event EventHandler ValueChanged;

    private int value;
    public int Value
    {
        get => this.value;

        set
        {
            if (this.value != value)
            {
                this.value = value;
                OnValueChanged(EventArgs.Empty);
            }
        }
    }

    protected virtual void OnValueChanged(EventArgs e)
    {
        this.ValueChanged?.Invoke(this, e);
    }
}

속성 값 Value의 값이 변경 되었을때 이벤트를 발생한다.
Null 조건 연산자를 사용하여 스레드로부터 안전한방식으로 대리자를 호출한다.

상속을 고려하지 않는다면 OnValueChanged 메서드를 전용(private) 으로 작성해도 무관하다.

다음은 이벤트처리 코드 이다.

Item item = new Item();

item.ValueChanged += (s, e) => 
{
    Console.WriteLine($"new value: {item.Value}");
};

item.Value = 10;

다음은 조금 수정하여 이벤트 데이터를 사용하여 변경전값과 현재값을 전달한다.

이벤트 데이터 정의:

class ValueChangedEventArgs : EventArgs
{
    public int OldValue { get; }
    public int NewValue { get; }

    public ValueChangedEventArgs(int oldValue, int newValue)
    {
        this.OldValue = oldValue;
        this.NewValue = newValue;
    }        
}

이벤트 데이터 사용:

class Item
{
    public event EventHandler<ValueChangedEventArgs> ValueChanged;

    private int value;
    public int Value
    {
        get => this.value;

        set
        {
            if (this.value != value)
            {
                var oldValue = this.value;
                this.value = value;
                OnValueChanged(new ValueChangedEventArgs(oldValue, this.value));
            }
        }
    }

    protected virtual void OnValueChanged(ValueChangedEventArgs e)
    {
        this.ValueChanged?.Invoke(this, e);
    }
}

이벤트 핸들러에서 데이터 사용:

Item item = new Item();

item.ValueChanged += (s, e) =>
{
    Console.WriteLine($"old value: {e.OldValue}, new value: {e.NewValue}");
};

item.Value = 10;

이번에는 여러 속성에서 사용 할 수 있도록 변경 한다.
속성명을 알아내기 위해 CallerMemberName Attribute 를 사용 했다.

수정된 전체 코드:

class PropertyValueChangedEventArgs : EventArgs
{
    public string PropertyName { get; }
    public object OldValue { get; }
    public object NewValue { get; }

    public PropertyValueChangedEventArgs(string propertyName, object oldValue, object newValue)
    {
        this.PropertyName = propertyName;
        this.OldValue = oldValue;
        this.NewValue = newValue;
    }
}

class Item
{
    public event EventHandler<PropertyValueChangedEventArgs> PropertyValueChanged;

    private int intValue;
    public int IntValue
    {
        get => this.intValue;

        set
        {
            if (this.intValue != value)
            {
                var oldValue = this.intValue;
                this.intValue = value;
                OnPropertyValueChanged(oldValue, this.intValue);
            }
        }
    }

    private string strValue;
    public string StrValue
    {
        get => this.strValue;

        set
        {
            if (this.strValue != value)
            {
                var oldValue = this.strValue;
                this.strValue = value;
                OnPropertyValueChanged(oldValue, this.strValue);
            }
        }
    }

    private void OnPropertyValueChanged(            
        object oldValue, object newValue,
        [CallerMemberName] string memberName = "")
    {
        this.PropertyValueChanged?.Invoke(this, new PropertyValueChangedEventArgs(memberName, oldValue, newValue));
    }
}

class Program
{
    static void Main(string[] args)
    {
        Item item = new Item();

        item.PropertyValueChanged += (s, e) =>
        {
            Console.WriteLine($"property name: {e.PropertyName} old value: {e.OldValue}, new value: {e.NewValue}");
        };

        item.IntValue = 10;
    }
}

2017/12/21

C# 6.0 Null 조건 연산자


하위 데이터 사용 시 null 검사를 하는 기존 방법:

var id = parent == null ? null : parent.ID;

C# 6.0 에 추가된 Null 조건 연산자를 사용하면 유용하다.

var id = parent?.ID;

var id = parent?.Parent?.ID;

인덱서에 Null 조건 연산자 사용:

var id = parents?[0]?.ID;

또한 스레드로부터 안전한 방식으로 대리자를 호출하는 데 사용된다.

기존 방식:

var handler = this.ValueChanged;
if(handler != null)
   handler(this, e);

Null 조건 연산자 사용:

this.ValueChanged?.Invoke(this, e);

2017/11/28

C# yield return, 확장 메서드를 이용하여 Enumerable.Where 만들기

Enumerable.Where 와 같은 동작을 하는 Where2 를 만들어 보자.

public static class EnumerableEx
{
    public static IEnumerable<TSource> Where2<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
    {
        foreach (var item in source)
        {
            if (predicate(item))
                yield return item;
        }
    }
}

확장 메서드로 만들어서 IEnumerable.Where2 로 사용 가능하며
yield return 으로 지연 반환을 한다.


List<int> list = new List<int>();

list.Add(1);
list.Add(2);
list.Add(3);

var result = list.Where2(i => i > 1);

foreach (var item in result) { }//result 2, 3

list.Add(4);

foreach (var item in result) { }//result 2, 3, 4

C# async await 를 이용한 안전한 WinForm Control 호출

Button을 클릭 하면 어떤 작업을 실행하고 실행 과정을 ProgressBar에 표시 한다고 가정하자.

이전 사용하던 번거로운 비동기 처리 작업과 Control.Invoke() 메서드를 사용하지 않고
async await 를 사용하여 간편하게 비동기 처리가 가능 하다.


private async void button1_Click(object sender, EventArgs e)
{
    int taskCount = 10;

    this.progressBar1.Minimum = 0;
    this.progressBar1.Maximum = taskCount;
    this.progressBar1.Value = 0;

    for (int i = 0; i < taskCount; i++)
    {
        await this.SometingAsync();

        this.progressBar1.Value++;
    }
}

private async Task SometingAsync()
{
    await Task.Delay(2000);
}

2017/07/04

WPF 컬렉션 바인딩 과 그룹, 필터 및 정렬

WPF 컬렉션 바인딩 과 필터 및 정렬

모델

ComboBox에서 보기 좋은 출력을 위해 DisplayName 을 추가 한다.
public class ProductType
{
    public string ProductTypeId { get; set; }
    public string ProductTypeName { get; set; }
    public string GroupName { get; set; }
    public string DisplayName
    {
        get { return $"{this.GroupName}, {this.ProductTypeName}"; }
    }
}
 
public class Product
{
    public string ProductId { get; set; }
    public string ProductName { get; set; }
    public int Price { get; set; }
    public string ProductTypeId { get; set; }
}

컬렉션 객체


ObservableCollection<ProductType> productTypes = new ObservableCollection<ProductType>();
ObservableCollection<Product> products = new ObservableCollection<Product>();
 
productTypes.Add(new ProductType() { ProductTypeId = "1", ProductTypeName = "Server", GroupName = "Month" });
productTypes.Add(new ProductType() { ProductTypeId = "2", ProductTypeName = "Desktop", GroupName = "Month" });
productTypes.Add(new ProductType() { ProductTypeId = "3", ProductTypeName = "Server", GroupName = "Permanent" });
productTypes.Add(new ProductType() { ProductTypeId = "4", ProductTypeName = "Desktop", GroupName = "Permanent" });
 
products.Add(new Product() { ProductId = "1", ProductName = "Azure SQL", Price = 250, ProductTypeId = "1" });
products.Add(new Product() { ProductId = "2", ProductName = "Azure Web App", Price = 222, ProductTypeId = "1" });
products.Add(new Product() { ProductId = "3", ProductName = "Office 365", Price = 600, ProductTypeId = "2" });
products.Add(new Product() { ProductId = "4", ProductName = "OneDrive", Price = 100, ProductTypeId = "2" });

View - 정렬 및 그룹


var productTypeView = CollectionViewSource.GetDefaultView(productTypes);
productTypeView.SortDescriptions.Add(new System.ComponentModel.SortDescription(nameof(ProductType.ProductTypeName), System.ComponentModel.ListSortDirection.Ascending));
productTypeView.GroupDescriptions.Add(new PropertyGroupDescription(nameof(ProductType.GroupName)));

View - 필터 및 정렬

Price 가 300 이하인 DataGrid 와 미만인 DataGrid 두개를 만들 것이다.
Price 업데이트 시 적용을 필터를 바로 적용하기 위하여 IsLiveFilteringRequested = true 를 사용하며 LiveFilteringProperties 에 Price 를 추가한다.
CollectionViewSource viewSource1 = new CollectionViewSource();
CollectionViewSource viewSource2 = new CollectionViewSource();
 
viewSource1.Source = this.products;
viewSource1.IsLiveFilteringRequested = true;
viewSource1.LiveFilteringProperties.Add(nameof(Product.Price));
viewSource1.View.Filter = (i) => (i as Product).Price <= 300;
 
viewSource2.Source = this.products;
viewSource2.IsLiveFilteringRequested = true;
viewSource2.LiveFilteringProperties.Add(nameof(Product.Price));
viewSource2.View.Filter = (i) => (i as Product).Price > 300;

소스 Binding


this.productTypeComboBox.ItemsSource = productTypeView;
this.gridProductTypeComboLowPrice.ItemsSource = productTypeView;
this.gridProductTypeComboHighPrice.ItemsSource = productTypeView;
 
this.dataGridLowPrice.ItemsSource = viewSource1.View;
this.dataGridHighPrice.ItemsSource = viewSource2.View;

Xaml

ComboBox는 그룹헤더로 구분 되여 출력을 한다.
<ComboBox Name="productTypeComboBox">
    <ComboBox.GroupStyle>
        <GroupStyle>
            <GroupStyle.HeaderTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Name}"/>
                </DataTemplate>
            </GroupStyle.HeaderTemplate>
        </GroupStyle>
    </ComboBox.GroupStyle>
    <ComboBox.ItemTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding ProductTypeName}"></TextBlock>
        </DataTemplate>
    </ComboBox.ItemTemplate>
</ComboBox>

DataGrid 에서 ProductType은 DisplayName을 출력 한다.
Price 300 이하은 DataGrid는 오름차순 정렬을 하고 초과하는 DataGrid는 내림차순 졍렬을 한다.
SelectedValueBinding, SelectedValuePath 두개의 속성으로 항목을 매핑을 하고 DisplayMemberPath로 표시 속성을 설정 한다.
<DataGrid Name="dataGridLowPrice" AutoGenerateColumns="False" >
    <DataGrid.Columns>
        <DataGridTextColumn Header="ProductName" Binding="{Binding ProductName}" />
        <DataGridTextColumn Header="Price" Binding="{Binding Price}" SortDirection="Ascending" />
        <DataGridComboBoxColumn x:Name="gridProductTypeComboLowPrice" Header="ProductType" SelectedValueBinding="{Binding ProductTypeId}" SelectedValuePath="ProductTypeId" DisplayMemberPath="DisplayName" />
    </DataGrid.Columns>
</DataGrid>                    
<DataGrid Name="dataGridHighPrice" AutoGenerateColumns="False" >
    <DataGrid.Columns>
        <DataGridTextColumn Header="ProductName" Binding="{Binding ProductName}" />
        <DataGridTextColumn Header="Price" Binding="{Binding Price}" SortDirection="Descending" />
        <DataGridComboBoxColumn x:Name="gridProductTypeComboHighPrice" Header="ProductType" SelectedValueBinding="{Binding ProductTypeId}" SelectedValuePath="ProductTypeId" DisplayMemberPath="DisplayName" />
    </DataGrid.Columns>
</DataGrid> 

결과 화면

ComboBox Group 적용
ComboBox Group 적용

DataGrid ComboBox DisplayName적용
DataGrid ComboBox DisplayName적용

2017/06/27

WPF 에서 WebApi 인증

WPF 에서 WebApi 인증

RoleProvider 생성


public class TestRoleProvider : RoleProvider
{
    //생략...
 
    public override string[] GetRolesForUser(string username)
    {
        string[] roles = { "Member" };//get roles by username
 
        return roles;
    }
}

RoleProvider 및 인증 설정


<system.web>
  <authentication mode="Forms">
    <forms loginUrl="~/member/login" timeout="20" slidingExpiration="true" />
  </authentication>
  <roleManager enabled="true" defaultProvider="TestRoleProvider">
    <providers>
      <clear />
      <add name="TestRoleProvider" type="WebApiTest.TestRoleProvider" />
    </providers>
  </roleManager>
</system.web>

권한 필터 설정


public class FilterConfig
{
    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new AuthorizeAttribute());
    }
}

Action에 권한 설정


public class ItemController : ApiController
{
    Item[] items = new Item[]
    {
    new Item { Id = "google", Name = "Google" },
    new Item { Id = "naver", Name = "Naver" },
    new Item { Id = "daum", Name = "Daum" }
    };
 
    [System.Web.Http.Authorize(Roles = "Member")]
    public IEnumerable<Item> GetAllItems()
    {
        return items;
    }
}

public class MemberController : Controller
{
    [HttpPost]
    [System.Web.Mvc.AllowAnonymous]
    public ActionResult Login(string userId, string password)
    {
        //사용자 유효성 검사
        if (userId == "test" && password == "1234")
        {
            //인증쿠키 설정
            System.Web.Security.FormsAuthentication.SetAuthCookie(userId, false);
            return new HttpStatusCodeResult(System.Net.HttpStatusCode.OK);
        }
 
        return new HttpStatusCodeResult(System.Net.HttpStatusCode.Unauthorized);
    }
}

HttpClient 로그인


private Uri uri = new Uri("http://localhost:59791");
private CookieContainer cookies = new CookieContainer();
private HttpClientHandler handler = new HttpClientHandler();
private HttpClient client;
 
private ItemCollection items = new ItemCollection();
 
public MainWindow()
{
    InitializeComponent();
 
    this.handler.CookieContainer = this.cookies;
    this.client = new HttpClient(this.handler);
 
    this.client.BaseAddress = uri;
 
    this.Login();
 
    this.client.DefaultRequestHeaders.Accept.Add(
        new MediaTypeWithQualityHeaderValue("application/json"));
 
 
    this.ItemsList.ItemsSource = this.items;
}
 
public HttpStatusCode Login()
{
    var result = this.client.PostAsync("member/login",
        new FormUrlEncodedContent(
        new Dictionary<string, string>
        {
            {"userId", "test"},
            {"password", "1234"}
        }
        )
    ).Result;
 
    return result.StatusCode;
}

WebApi 사용


private async void GetItems(object sender, RoutedEventArgs e)
{
    var response = await client.GetAsync("api/item/getallitems");
    response.EnsureSuccessStatusCode();
 
    var items = await response.Content.ReadAsAsync<IEnumerable<Item>>();
    this.items.CopyFrom(items);
}

WPF 에서 WebApi 사용하기

WPF 에서 WebApi 사용하기

Model

public class Item
{
    public string Id { get; set; }
    public string Name { get; set; }
}

WebAPI

public class ItemController : ApiController
{
    Item[] items = new Item[]
    {
        new Item { Id = "google", Name = "Google" },
        new Item { Id = "naver", Name = "Naver" },
        new Item { Id = "daum", Name = "Daum" }
    };
 
    public IEnumerable<Item> GetAllItems()
    {
        return items;
    }
}

WPF ObservableCollection

public class ItemCollection : ObservableCollection<Item>
{
    public void CopyFrom(IEnumerable<Item> items)
    {
        this.Items.Clear();
        foreach (var p in items)
        {
            this.Items.Add(p);
        }
 
        this.OnCollectionChanged(
            new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
}

WPF View

<StackPanel Width="250" >
    <Button Name="getAllItemsButton" Click="GetItems">Get Items</Button>
    <ListBox Name="ItemsList">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <StackPanel Margin="2">
                    <TextBlock >Id: <Run Text="{Binding Path=Id}" /></TextBlock>
                    <TextBlock >Name: <Run Text="{Binding Path=Name}" /></TextBlock>
                </StackPanel>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
</StackPanel>

WPF Code

비동기 호출을 사용하여 사용자 응답성을 향상 시킨다.

public partial class MainWindow : Window
{
    HttpClient client = new HttpClient();
    ItemCollection items = new ItemCollection();
 
    public MainWindow()
    {
        InitializeComponent();
 
        client.BaseAddress = new Uri("http://localhost:59791");
        client.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
 
        this.ItemsList.ItemsSource = this.items;
    }
 
    private async void GetItems(object sender, RoutedEventArgs e)
    {
        try
        {
            this.getAllItemsButton.IsEnabled = false;
 
            var response = await client.GetAsync("api/item/getallitems");
            response.EnsureSuccessStatusCode(); 
 
            var items = await response.Content.ReadAsAsync<IEnumerable<Item>>();
            this.items.CopyFrom(items);
        }
        catch (Newtonsoft.Json.JsonException jEx)
        {
            MessageBox.Show(jEx.Message);
        }
        catch (HttpRequestException ex)
        {
            MessageBox.Show(ex.Message);
        }
        finally
        {
            this.getAllItemsButton.IsEnabled = true;
        }
    }
}

C# 문자열 포함 여부 확인하기.

ToUpper() 를 사용하면 불필요한 문자열을 생성하므로 좋은 방법은 아니다. string text = "This is an apple." ; string apple = "Apple." ; bool ...