View-State는 Flow 내에서 화면을 생성하는 요소이다.
여기서는 View-State에 대해서 알아보도록 하자.
View-State는 기본적으로 해당 뷰를 생성하여 보여준 후,
사용자가 화면을 통해 응답을 하는 것을 기다린다.
아래는 view-state는 enterBookingDetails라는 ID를 가지고 있으며
또한 별도의 view를 지정하는 설정이 없으면 ID가 곧 view를 뜻한다.
<view-state id="enterBookingDetails"> <transition on="submit" to="reviewBooking" /> </view-state>
따라서. 디렉토리 상의
booking.xml(or booking-flow.xml)이 존재하는 디렉토리에 있는 enterBookingDetails.jsp이 자동으로 view 로 동작한다.
또는 절대경로를 이용하여 명시적으로 view=”/WEB-INF/hotels/booking/enterBookingDetails.jsp” 설정할 수도 있다.
아래에서 다시 설명하겠다.
뷰 속성을 여러 방법으로 지정할 수 있다.
<view-state id="enterBookingDetails" view="bookingDetails.xhtml">
<view-state id="enterBookingDetails" view="/WEB-INF/hotels/booking/bookingDetails.jsp>
<view-state id="enterBookingDetails" view="bookingDetails">
view-state 내부에서 유지되는 변수. Ajax 요청처럼 동일한 뷰가 여러번 보여줘야 하는 경우 유용.
<var name="searchCriteria" class="com.mycompany.myapp.hotels.SearchCriteria" />
<on-render> <evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels"/> </on-render>
아래코드는 화면 ID 가 searchResults 인 view-state 화면을 그리되
그리기전 bookingService.findHotels(searchCriteria) 메소드를 호출한 후 그 결과를 viewScope내의 hotels 로 저장한 후 화면을 보여주고 있다.
그리고 화면상에 next 또는 previous 이벤트 발생시 eval(searchCriteria.nextPage()/previousPage()) 이 발생
그 결과를 fragments으로 지정된 영역에 뿌려주고 있다.
<view-state id="searchResults"> <on-render> <evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" /> </on-render> <transition on="next"> <evaluate expression="searchCriteria.nextPage()" /> <render fragments="searchResultsFragment" /> </transition> <transition on="previous"> <evaluate expression="searchCriteria.previousPage()" /> <render fragments="searchResultsFragment" /> </transition> </view-state>
자세한 것은 아래에서 다시 설명하도록 하겠다.
뷰를 보여주기 전에 특정 액션을 실행하려면 on-render 사용한다.
<on-render> <evaluate expression="bookingService.findHotels(searchCriteria)" result="viewScope.hotels" /> </on-render>
<view-state id="enterBookingDetails" model="booking">
뷰 이벤트가 발생했을 때 지정된 모델에 대해서 다음 행동이 일어난다.
org.springframework.binding.convert.converters.TwoWayConverter을 구현하면 됨. StringToObject를 구현하는게 더 좋다.
protected abstract Object toObject(String string, Class targetClass) throws Exception; protected abstract String toString(Object object) throws Exception;
구현 예.
public class StringToMonetaryAmount extends StringToObject { public StringToMonetaryAmount() { super(MonetaryAmount.class); } @Override protected Object toObject(String string, Class targetClass) { return MonetaryAmount.valueOf(string); } @Override protected String toString(Object object) { MonetaryAmount amount = (MonetaryAmount) object; return amount.toString(); } }
org.springframework.binding.convert.converters에 이미 구현된 변환기가 위치.
org.springframework.binding.convert.service.DefaultConversionService을 상속해서 addDefaultConverters() 메소드를 재정의 하면 된다.
자세한 것은 시스템 설정 에서 ConversionService 확장을 이용하여 설정하는 곳에서 다루고 있다.
bind 속성으로 특정 뷰 이벤트에서 모델 바인딩과 유효성 검증을 안 하게 할 수도 있다.
<view-state id="enterBookingDetails" model="booking"> <transition on="proceed" to="reviewBooking"> <transition on="cancel" to="bookingCancelled" bind="false" /> </view-state>
아래와 같이 binder 속성으로 바인딩 할 프로퍼티를 명시적으로 지정할 수 있다.
<view-state id="enterBookingDetails" model="booking"> <binder> <binding property="creditCard" /> <binding property="creditCardName" /> <binding property="creditCardExpiryMonth" /> <binding property="creditCardExpiryYear" /> </binder> <transition on="proceed" to="reviewBooking" /> <transition on="cancel" to="cancel" bind="false" /> </view-state>
binder로 지정하지 않으면 모든 프로퍼티를 바인딩된다. converter를 이용하여 변환기 지정 가능하다.
<view-state id="enterBookingDetails" model="booking"> <binder> <binding property="checkinDate" converter="shortDate" /> <binding property="checkoutDate" converter="shortDate" /> <binding property="creditCard" /> <binding property="creditCardName" /> <binding property="creditCardExpiryMonth" /> <binding property="creditCardExpiryYear" /> </binder> <transition on="proceed" to="reviewBooking" /> <transition on="cancel" to="cancel" bind="false" /> </view-state>
Model 유효성 검사에 대한 부분은 Web flow 에서는 프로그래밍적으로 제약사항을 강제화 하는 형태로 지원하고 있다.
첫 번째 방법으로 유효성 검증 로직을 모델 객체 내에 정의하는 방법이다.
Web Flow 는 view-stat 에서 모델로 넘어간 시점(view-state postback lifecycle)에서 자동적으로 validate 메소드를 자동으로 호출한다.
<view-state id="enterBookingDetails" model="booking"> <transition on="proceed" to="reviewBooking"> </view-state>
Booking class내의 validate{view-state 명) 코드는 아래와 같이 볼 수 있다.(메소드명 : validate + EnterBookingDetails)
public class Booking { private Date checkinDate; private Date checkoutDate; ... public void validateEnterBookingDetails(ValidationContext context) { MessageContext messages = context.getMessages(); if (checkinDate.before(today())) { messages.addMessage(new MessageBuilder().error().source("checkinDate"). defaultText("Check in date must be a future date").build()); } else if (!checkinDate.before(checkoutDate)) { messages.addMessage(new MessageBuilder().error() .source("checkoutDate") .defaultText("Check out date must be later than check in date") .build()); } } }
enterBookingDetails에 대한 이벤트가 발생했을 때 자동으로 validateEnterBookingDetails이 호출 된다.
메소드 이름을 validate$<state> 로 정의하면 된다.
Validator로 불리는 별도의 객체로 정의할 수도 있다. 클래스 이름을 $<model>Validator 로 지정하면 된다.
메소드 이름은 역시 validate$<state>로 한다.
아래 클래스명은 Booking + Validator 이며 메소드 이름은 validate + EnterBookingDetails 임을 볼 수 있다.
@Component public class BookingValidator { public void validateEnterBookingDetails(Booking booking, ValidationContext context) { MessageContext messages = context.getMessages(); if (booking.getCheckinDate().before(today())) { messages.addMessage(new MessageBuilder().error() .source("checkinDate") .defaultText("Check in date must be a future date") .build()); } else if (!booking.getCheckinDate().before(booking.getCheckoutDate())) { messages.addMessage(new MessageBuilder().error() .source("checkoutDate") .defaultText("Check out date must be later than check in date") .build()); } } }
spring mvc의 Error 객체도 받을 수 있다.
유효성 검증 동안에 MessageContext에 접근할 수 있게 해주며, 다양한 객체에 접근 가능하게 해준다.
validate=“false” 설정함으로 유효성 검사를 하지 않을 수 있다
<view-state id="chooseAmenities" model="booking"> <transition on="proceed" to="reviewBooking"> <transition on="back" to="enterBookingDetails" validate="false" /> </view-state>
전이 대상은 (1)다른 뷰, (2)현재 뷰를 다시, (3)action을 실행, (4)Ajax 이벤트를 제어할 때
'fragments'로 불리는 일부 뷰를 보여주라는 요청일 수도 있다.
<transition on="submit" to="bookingConfirmed"> <evaluate expression="bookingAction.makeBooking(booking, messageContext)" /> </transition>
public class BookingAction { public boolean makeBooking(Booking booking, MessageContext context) { try { bookingService.make(booking); return true; } catch (RoomNotAvailableException e) { context.addMessage(builder.error().defaultText("No room is available at this hotel").build()); return false; } } }
<global-transitions> <transition on="login" to="login"> <transition on="logout" to="logout"> </global-transitions>
<transition on="event"> <!-- Handle event --> </transition>
현재 뷰 중 일부만을 다시 보여줄 수 있는 방법으로, Ajax 기반일 때 주로 사용한다.
<transition on="next"> <evaluate expression="searchCriteria.nextPage()" /> <render fragments="searchResultsFragment" /> </transition>
','로 구분해서 다수의 fragment를 지정할 수도 있다.
MessageContext는 플로우 실행 동안에 메세지를 저장하는데 사용되는 API다.
일반 메세지나 국제화가 지원된 메세지 모두 사용 가능하다.
메세지 수준도 지정 가능하며, 지원되는 수준은 info, warning, error이 있다. 메세지를 추가할 때는 MessageBuilder를 사용하자.
MessageContext context = ... MessageBuilder builder = new MessageBuilder(); context.addMessage(builder.error().source("checkinDate").defaultText("Check in date must be a future date").build()); context.addMessage(builder.warn().source("smoking").defaultText("Smoking is bad for your health").build()); context.addMessage(builder.info().defaultText("We have processed your reservation - thank you and enjoy your stay").build());
MessageContext context = ... MessageBuilder builder = new MessageBuilder(); context.addMessage(builder.error().source("checkinDate").code("checkinDate.notFuture").build()); context.addMessage(builder.warn().source("smoking").code("notHealthy").resolvableArg("smoking").build());
스프링의 MessageSource를 사용해서 메세지 번들을 정의가 가능하다. 간단히 프로퍼티 파일로 관리하면 된다.
#messages.properties checkinDate=Check in date must be a future date notHealthy={0} is bad for your health reservationConfirmation=We have processed your reservation - thank you and enjoy your stay
뷰나 플로우에서는 resourceBundle EL 변수로 접근도 가능하다.
<h:outputText value="#{resourceBundle.reservationConfirmation}" />
시스템에서 발생한 예외에 대해 메세지 지정 가능하다. 예를 들어 타입 변환 시 예외가 발생하면 typeMismatch를 통해서 메세지 지정 가능하다.
booking.checkinDate.typeMismatch=The check in date must be in the format yyyy-mm-dd.
모달 팝업 다이얼로그를 뷰로 렌더링하고 싶다면, view-state 내에 popup=“true” 설정하면 된다.
<view-state id="changeSearchCriteria" view="enterSearchCriteria.xhtml" popup="true">
특히 스프링 자바 스크립트와 함께 사용하면, 팝업을 보여주는데 클라이언트 코드가 전혀 필요 없다.
SWF가 클라이언트 요청을 팝업으로 재전송(redirect)해준다.
기본적으로 브라우저의 백 버튼으로 이전 view-state로 돌아갈 수 있다. history를 사용해서 이에 대한 설정이 가능하다.
<transition on="cancel" to="bookingCancelled" history="discard">
<transition on="confirm" to="bookingConfirmed" history="invalidate">